Skip to content
Closed
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ llm:
agentapi --cliproxy http://localhost:8317
```

## Managed service (Windows / Linux / macOS)

Use the repo service helper to install/start/stop/restart from one command.

```bash
task service:install # install platform service scaffold
task service:start # start service
task service:status # inspect service status
task service:restart # restart service
task service:stop # stop service
```

Platform behavior:

- Linux: manages `cliproxyapi-plusplus.service` via systemd.
- macOS: manages `com.router-for-me.cliproxyapi-plusplus` via launchd.
- Windows: uses `examples/windows/cliproxyapi-plusplus-service.ps1` for install/start/stop/status.

Comment on lines +160 to +172
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify Windows restart support in docs.

The command list includes task service:restart, but Windows behavior text omits restart. Align both to avoid ambiguity.

Proposed doc tweak
-- Windows: uses `examples/windows/cliproxyapi-plusplus-service.ps1` for install/start/stop/status.
+- Windows: uses `examples/windows/cliproxyapi-plusplus-service.ps1` for install/start/stop/restart/status.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 160 - 172, The documentation lists task
service:restart but the Windows platform note omits restart; update the README
so the Windows section either documents that restart is supported and how (e.g.,
call the same examples/windows/cliproxyapi-plusplus-service.ps1 with a restart
action) or explicitly states it’s not supported, and if you choose to support it
add a Restart-Service implementation to
examples/windows/cliproxyapi-plusplus-service.ps1; ensure the README text
mentions task service:restart alongside the existing install/start/stop/status
and references the PowerShell script when describing Windows behavior.

## Development

```bash
Expand Down
38 changes: 32 additions & 6 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ tasks:
- task: quality:vet
- task: quality:staticcheck
- task: quality:shellcheck
- task: service:test
- task: lint:changed

test:provider-smoke-matrix:test:
Expand All @@ -356,25 +357,19 @@ tasks:
cmds:
- task: preflight
- task: quality:docs-open-items-parity
<<<<<<< HEAD
- task: quality:docs-phase-placeholders
=======
>>>>>>> archive/pr-234-head-20260223
- ./.github/scripts/release-lint.sh

quality:docs-open-items-parity:
desc: "Prevent stale status drift in fragmented open-items report"
cmds:
- ./.github/scripts/check-open-items-fragmented-parity.sh

<<<<<<< HEAD
quality:docs-phase-placeholders:
desc: "Reject unresolved placeholder-like tokens in planning reports"
cmds:
- ./.github/scripts/check-phase-doc-placeholder-tokens.sh

=======
>>>>>>> archive/pr-234-head-20260223
test:smoke:
desc: "Run smoke tests for startup and control-plane surfaces"
deps: [preflight, cache:unlock]
Expand Down Expand Up @@ -484,6 +479,37 @@ tasks:
cmds:
- docker compose down

# -- Service Operations --
service:status:
desc: "Show service status for installed system service"
cmds:
- ./scripts/cliproxy-service.sh status

service:start:
desc: "Start cliproxyapi++ service (systemd/launchd/Windows helper)"
cmds:
- ./scripts/cliproxy-service.sh start

service:stop:
desc: "Stop cliproxyapi++ service (systemd/launchd/Windows helper)"
cmds:
- ./scripts/cliproxy-service.sh stop

service:restart:
desc: "Restart cliproxyapi++ service (systemd/launchd/Windows helper)"
cmds:
- ./scripts/cliproxy-service.sh restart

service:install:
desc: "Install cliproxyapi++ service scaffold (systemd/launchd)"
cmds:
- ./scripts/cliproxy-service.sh install

service:test:
desc: "Run cliproxy service helper contract tests"
cmds:
- ./scripts/cliproxy-service-test.sh
Comment on lines +482 to +511
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

service:start, service:stop, service:restart, and service:install invoke mutating system operations without any guard or dry-run mode.

Running task service:install or task service:start directly modifies the system (creates a launchd/systemd unit, starts a daemon). These are safe to list as DX helpers, but consider adding a confirmation prompt or an --install flag guard to avoid accidental execution. Also, service:test is placed before the fast lint:changed step in quality:ci (Line 347); if the test script is slow, it unnecessarily delays the lint gate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Taskfile.yml` around lines 482 - 511, The Task targets service:start,
service:stop, service:restart, and service:install currently perform mutating
system operations unguarded; change them to require an explicit guard (e.g.,
only run when a TASK_CONFIRM or TASK_FORCE env var is set or when an explicit
--install/--force flag is passed) or implement an interactive confirmation
prompt before invoking ./scripts/cliproxy-service.sh, and ensure service:test is
moved later in the quality:ci sequence (after lint:changed) so slow service
helper tests do not block the fast lint gate; update the Taskfile targets
service:start/service:stop/service:restart/service:install and the quality:ci
target to implement these guards and reorder service:test accordingly.


# -- Health & Diagnostics (UX/DX) --
doctor:
desc: "Check environment health for cliproxyapi++"
Expand Down
27 changes: 19 additions & 8 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func setKiroIncognitoMode(cfg *config.Config, useIncognito, noIncognito bool) {
}
}

func validateKiroIncognitoFlags(useIncognito, noIncognito bool) error {
if useIncognito && noIncognito {
return fmt.Errorf("--incognito and --no-incognito cannot be used together")
}
return nil
}

// main is the entry point of the application.
// It parses command-line flags, loads configuration, and starts the appropriate
// service based on the provided flags (login, codex-login, or server mode).
Expand Down Expand Up @@ -152,9 +159,13 @@ func main() {

// Parse the command-line flags.
flag.Parse()
var err error
if err = validateKiroIncognitoFlags(useIncognito, noIncognito); err != nil {
log.Errorf("invalid Kiro browser flags: %v", err)
return
}

// Core application variables.
var err error
var cfg *config.Config
var isCloudDeploy bool
var (
Expand Down Expand Up @@ -620,15 +631,15 @@ func main() {
}
}
} else {
// Start the main proxy service
managementasset.StartAutoUpdater(context.Background(), configFilePath)
// Start the main proxy service
managementasset.StartAutoUpdater(context.Background(), configFilePath)

if cfg.AuthDir != "" {
kiro.InitializeAndStart(cfg.AuthDir, cfg)
defer kiro.StopGlobalRefreshManager()
}
if cfg.AuthDir != "" {
kiro.InitializeAndStart(cfg.AuthDir, cfg)
defer kiro.StopGlobalRefreshManager()
}

cmd.StartService(cfg, configFilePath, password)
cmd.StartService(cfg, configFilePath, password)
}
}
}
2 changes: 1 addition & 1 deletion cmd/server/main_kiro_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package main
import (
"testing"

"github.com/router-for-me/CLIProxyAPI/v6/pkg/llmproxy/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
)

func TestValidateKiroIncognitoFlags(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/management/auth_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -2876,7 +2876,7 @@ func (h *Handler) RequestKiloToken(c *gin.Context) {
Metadata: map[string]any{
"email": status.UserEmail,
"organization_id": orgID,
"model": defaults.Model,
"model": defaults.Model,
},
}

Expand Down
13 changes: 11 additions & 2 deletions internal/api/modules/amp/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,21 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi
// Modify incoming responses to handle gzip without Content-Encoding
// This addresses the same issue as inline handler gzip handling, but at the proxy level
proxy.ModifyResponse = func(resp *http.Response) error {
method := ""
path := ""
if resp.Request != nil {
method = resp.Request.Method
if resp.Request.URL != nil {
path = resp.Request.URL.Path
}
}

// Log upstream error responses for diagnostics (502, 503, etc.)
// These are NOT proxy connection errors - the upstream responded with an error status
if resp.StatusCode >= 500 {
log.Errorf("amp upstream responded with error [%d] for %s %s", resp.StatusCode, resp.Request.Method, resp.Request.URL.Path)
log.Errorf("amp upstream responded with error [%d] for %s %s", resp.StatusCode, method, path)
} else if resp.StatusCode >= 400 {
log.Warnf("amp upstream responded with client error [%d] for %s %s", resp.StatusCode, resp.Request.Method, resp.Request.URL.Path)
log.Warnf("amp upstream responded with client error [%d] for %s %s", resp.StatusCode, method, path)
}

// Only process successful responses for gzip decompression
Expand Down
10 changes: 5 additions & 5 deletions internal/auth/copilot/copilot_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const (
copilotAPIEndpoint = "https://api.githubcopilot.com"

// Common HTTP header values for Copilot API requests.
copilotUserAgent = "GithubCopilot/1.0"
copilotEditorVersion = "vscode/1.100.0"
copilotPluginVersion = "copilot/1.300.0"
copilotIntegrationID = "vscode-chat"
copilotOpenAIIntent = "conversation-panel"
copilotUserAgent = "GithubCopilot/1.0"
copilotEditorVersion = "vscode/1.100.0"
copilotPluginVersion = "copilot/1.300.0"
copilotIntegrationID = "vscode-chat"
copilotOpenAIIntent = "conversation-panel"
)

// CopilotAPIToken represents the Copilot API token response.
Expand Down
11 changes: 4 additions & 7 deletions internal/auth/kiro/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,17 +506,14 @@ func GenerateTokenFileName(tokenData *KiroTokenData) string {
return fmt.Sprintf("kiro-%s-%s.json", authMethod, sanitizedEmail)
}

// Generate sequence only when email is unavailable
seq := time.Now().UnixNano() % 100000

// Priority 2: For IDC, use startUrl identifier with sequence
// Priority 2: For IDC, use startUrl identifier
if authMethod == "idc" && tokenData.StartURL != "" {
identifier := ExtractIDCIdentifier(tokenData.StartURL)
if identifier != "" {
return fmt.Sprintf("kiro-%s-%s-%05d.json", authMethod, identifier, seq)
return fmt.Sprintf("kiro-%s-%s.json", authMethod, identifier)
}
}

// Priority 3: Fallback to authMethod only with sequence
return fmt.Sprintf("kiro-%s-%05d.json", authMethod, seq)
// Priority 3: Fallback to authMethod only
return fmt.Sprintf("kiro-%s.json", authMethod)
Comment on lines +509 to +518
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Token filename collisions when email is unavailable and multiple accounts share the same IDC startURL or authMethod.

The comment (Line 492) says the sequence suffix was only needed "when email is unavailable", yet both the IDC-identifier path (Line 513) and the fallback path (Line 518) have now dropped it entirely. Two users authenticating via the same IDC tenant (startURL) without email will produce the same filename (kiro-idc-d-1234567890.json), causing one token to silently overwrite the other. The fallback path is even broader — all accounts with the same authMethod and no email/startURL collide on a single file.

🐛 Proposed fix — restore a stable disambiguator for the non-email paths
	// Priority 2: For IDC, use startUrl identifier
	if authMethod == "idc" && tokenData.StartURL != "" {
		identifier := ExtractIDCIdentifier(tokenData.StartURL)
		if identifier != "" {
-			return fmt.Sprintf("kiro-%s-%s.json", authMethod, identifier)
+			// Stable hash of startURL avoids collisions when multiple IDC
+			// tenants share the same subdomain prefix.
+			return fmt.Sprintf("kiro-%s-%s.json", authMethod, identifier)
+			// NOTE: If multi-user-per-tenant support is needed, re-add a
+			// sequence suffix here (e.g. -1, -2) or use a profile identifier.
		}
	}

-	// Priority 3: Fallback to authMethod only
-	return fmt.Sprintf("kiro-%s.json", authMethod)
+	// Priority 3: Fallback to authMethod only — risk of collision for
+	// multiple accounts; restore sequence suffix if multi-account needed.
+	return fmt.Sprintf("kiro-%s.json", authMethod)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auth/kiro/aws.go` around lines 509 - 518, The filename generation
currently drops the "sequence" disambiguator causing token files to collide when
email is absent; update the IDC branch and the fallback to append a stable
disambiguator derived from the token (instead of removing it). Implement a
helper like computeTokenDisambiguator(tokenData) that returns a short stable
string (prefer an existing stable id field such as tokenData.Sub / AccountID if
available, or a short hash of StartURL+authMethod+tokenData.Sub), then change
the two returns in the function that use ExtractIDCIdentifier,
tokenData.StartURL and authMethod to include that disambiguator (e.g.,
fmt.Sprintf("kiro-%s-%s-%s.json", authMethod, identifierOrEmpty, disambiguator)
and fmt.Sprintf("kiro-%s-%s.json", authMethod, disambiguator)). Ensure the
helper is deterministic and short to avoid filename length issues and use it
wherever the code previously relied on the omitted "sequence" suffix.

}
10 changes: 5 additions & 5 deletions internal/auth/kiro/aws_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ const (
// Note: This is different from the Amazon Q streaming endpoint (q.us-east-1.amazonaws.com)
// used in kiro_executor.go for GenerateAssistantResponse. Both endpoints are correct
// for their respective API operations.
awsKiroEndpoint = "https://codewhisperer.us-east-1.amazonaws.com"
defaultTokenFile = "~/.aws/sso/cache/kiro-auth-token.json"
targetGetUsage = "AmazonCodeWhispererService.GetUsageLimits"
targetListModels = "AmazonCodeWhispererService.ListAvailableModels"
targetGenerateChat = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse"
awsKiroEndpoint = "https://codewhisperer.us-east-1.amazonaws.com"
defaultTokenFile = "~/.aws/sso/cache/kiro-auth-token.json"
targetGetUsage = "AmazonCodeWhispererService.GetUsageLimits"
targetListModels = "AmazonCodeWhispererService.ListAvailableModels"
targetGenerateChat = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse"
)

// KiroAuth handles AWS CodeWhisperer authentication and API communication.
Expand Down
24 changes: 12 additions & 12 deletions internal/auth/kiro/codewhisperer_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ type CodeWhispererClient struct {

// UsageLimitsResponse represents the getUsageLimits API response.
type UsageLimitsResponse struct {
DaysUntilReset *int `json:"daysUntilReset,omitempty"`
NextDateReset *float64 `json:"nextDateReset,omitempty"`
UserInfo *UserInfo `json:"userInfo,omitempty"`
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"`
UsageBreakdownList []UsageBreakdown `json:"usageBreakdownList,omitempty"`
DaysUntilReset *int `json:"daysUntilReset,omitempty"`
NextDateReset *float64 `json:"nextDateReset,omitempty"`
UserInfo *UserInfo `json:"userInfo,omitempty"`
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"`
UsageBreakdownList []UsageBreakdown `json:"usageBreakdownList,omitempty"`
}

// UserInfo contains user information from the API.
Expand All @@ -49,13 +49,13 @@ type SubscriptionInfo struct {

// UsageBreakdown contains usage details.
type UsageBreakdown struct {
UsageLimit *int `json:"usageLimit,omitempty"`
CurrentUsage *int `json:"currentUsage,omitempty"`
UsageLimitWithPrecision *float64 `json:"usageLimitWithPrecision,omitempty"`
CurrentUsageWithPrecision *float64 `json:"currentUsageWithPrecision,omitempty"`
NextDateReset *float64 `json:"nextDateReset,omitempty"`
DisplayName string `json:"displayName,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
UsageLimit *int `json:"usageLimit,omitempty"`
CurrentUsage *int `json:"currentUsage,omitempty"`
UsageLimitWithPrecision *float64 `json:"usageLimitWithPrecision,omitempty"`
CurrentUsageWithPrecision *float64 `json:"currentUsageWithPrecision,omitempty"`
NextDateReset *float64 `json:"nextDateReset,omitempty"`
DisplayName string `json:"displayName,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
}

// NewCodeWhispererClient creates a new CodeWhisperer client.
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/kiro/cooldown.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
)

const (
CooldownReason429 = "rate_limit_exceeded"
CooldownReasonSuspended = "account_suspended"
CooldownReason429 = "rate_limit_exceeded"
CooldownReasonSuspended = "account_suspended"
CooldownReasonQuotaExhausted = "quota_exhausted"

DefaultShortCooldown = 1 * time.Minute
Expand Down
6 changes: 3 additions & 3 deletions internal/auth/kiro/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var (
"1.0.20", "1.0.21", "1.0.22", "1.0.23",
"1.0.24", "1.0.25", "1.0.26", "1.0.27",
}
osTypes = []string{"darwin", "windows", "linux"}
osTypes = []string{"darwin", "windows", "linux"}
osVersions = map[string][]string{
"darwin": {"14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "15.0", "15.1"},
"windows": {"10.0.19041", "10.0.19042", "10.0.19043", "10.0.19044", "10.0.22621", "10.0.22631"},
Expand Down Expand Up @@ -67,9 +67,9 @@ var (
"1366x768", "1440x900", "1680x1050",
"2560x1600", "3440x1440",
}
colorDepths = []int{24, 32}
colorDepths = []int{24, 32}
hardwareConcurrencies = []int{4, 6, 8, 10, 12, 16, 20, 24, 32}
timezoneOffsets = []int{-480, -420, -360, -300, -240, 0, 60, 120, 480, 540}
timezoneOffsets = []int{-480, -420, -360, -300, -240, 0, 60, 120, 480, 540}
)

// NewFingerprintManager 创建指纹管理器
Expand Down
6 changes: 3 additions & 3 deletions internal/auth/kiro/jitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const (
)

var (
jitterRand *rand.Rand
jitterRandOnce sync.Once
jitterMu sync.Mutex
jitterRand *rand.Rand
jitterRandOnce sync.Once
jitterMu sync.Mutex
lastRequestTime time.Time
)

Expand Down
8 changes: 4 additions & 4 deletions internal/auth/kiro/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ type TokenScorer struct {
metrics map[string]*TokenMetrics

// Scoring weights
successRateWeight float64
quotaWeight float64
latencyWeight float64
lastUsedWeight float64
successRateWeight float64
quotaWeight float64
latencyWeight float64
lastUsedWeight float64
failPenaltyMultiplier float64
}

Expand Down
4 changes: 2 additions & 2 deletions internal/auth/kiro/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import (
const (
// Kiro auth endpoint
kiroAuthEndpoint = "https://prod.us-east-1.auth.desktop.kiro.dev"

// Default callback port
defaultCallbackPort = 9876

// Auth timeout
authTimeout = 10 * time.Minute
)
Expand Down
Loading
Loading