Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Run linters
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v7
with:
version: latest
version: v2.1
args: --timeout=3m
go-test:
strategy:
Expand Down
174 changes: 77 additions & 97 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,103 +1,83 @@
linters-settings:
exhaustive:
default-signifies-exhaustive: true

gocritic:
# The list of supported checkers can be find in https://go-critic.github.io/overview.
settings:
underef:
# Whether to skip (*x).method() calls where x is a pointer receiver.
skipRecvDeref: false

govet:
enable-all: true
disable:
- fieldalignment # too strict
- shadow # complains too much about shadowing errors. All research points to this being fine.

nakedret:
max-func-lines: 0

nolintlint:
allow-no-explanation: [ forbidigo, tracecheck, gomnd, gochecknoinits, makezero ]
require-explanation: true
require-specific: true

revive:
ignore-generated-header: true
severity: error
rules:
- name: atomic
- name: line-length-limit
arguments: [ 200 ]
# These are functions that we use without checking the errors often. Most of these can't return an error even
# though they implement an interface that can.
- name: unhandled-error
arguments:
- fmt.Printf
- fmt.Println
- fmt.Fprintf
- fmt.Fprintln
- os.Stderr.Sync
- sb.WriteString
- buf.WriteString
- hasher.Write
- os.Setenv
- os.RemoveAll
- name: var-naming
arguments: [["ID", "URL", "HTTP", "API"], []]

tenv:
all: true

varcheck:
exported-fields: false # this appears to improperly detect exported variables as unused when they are used from a package with the same name

version: "2"

linters:
disable-all: true
enable:
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unused # Checks Go code for unused constants, variables, functions and types
- asasalint # Check for pass []any as any in variadic func(...any)
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- durationcheck # check for two durations multiplied together
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
- exhaustive # check exhaustiveness of enum switch statements
- forbidigo # Forbids identifiers
- gochecknoinits # Checks that no init functions are present in Go code
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # Provides diagnostics that check for bugs, performance and style issues.
- godot # Check if comments end in a period
- goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt.
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
- goprintffuncname # Checks that printf-like functions are named with f at the end
- gosec # Inspects source code for security problems
- nakedret # Finds naked returns in functions greater than a specified function length
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
- noctx # noctx finds sending http request without context.Context
- nolintlint # Reports ill-formed or insufficient nolint directives
- nonamedreturns # Reports all named returns
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
- predeclared # find code that shadows one of Go's predeclared identifiers
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
- unconvert # Remove unnecessary type conversions
- usestdlibvars # detect the possibility to use variables/constants from the Go standard library
- whitespace # Tool for detection of leading and trailing whitespace
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- asasalint
- asciicheck
- bidichk
- bodyclose
- durationcheck
- errorlint
- exhaustive
- forbidigo
- gochecknoinits
- goconst
- gocritic
- godot
- gomoddirectives
- goprintffuncname
- gosec
- nakedret
- nilerr
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- predeclared
- revive
- tparallel
- unconvert
- usestdlibvars
- whitespace
settings:
exhaustive:
default-signifies-exhaustive: true
goconst:
min-occurrences: 5
gocritic:
settings:
underef:
skipRecvDeref: false
govet:
enable-all: true
disable:
- fieldalignment
- shadow
nakedret:
max-func-lines: 0
nolintlint:
allow-no-explanation: [forbidigo, tracecheck, gomnd, gochecknoinits, makezero]
require-explanation: true
require-specific: true
revive:
severity: error
rules:
- name: atomic
- name: line-length-limit
arguments: [200]
- name: unhandled-error
arguments:
- fmt.Printf
- fmt.Println
- fmt.Fprintf
- fmt.Fprintln
- os.Stderr.Sync
- sb.WriteString
- buf.WriteString
- hasher.Write
- os.Setenv
- os.RemoveAll
- name: var-naming
arguments: [["ID", "URL", "HTTP", "API"], []]

formatters:
enable:
- goimports

issues:
max-same-issues: 50

exclude-rules:
# Don't require TODO comments to end in a period
- source: "(TODO)"
linters: [ godot ]
8 changes: 4 additions & 4 deletions cmd/cone/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func createAWSProfile(entitlement *shared.AppEntitlement, resource *shared.AppRe
}

configPath := filepath.Join(awsConfigDir, "config")
configContent, err := os.ReadFile(configPath)
configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("failed to read AWS config: %w", err)
}
Expand Down Expand Up @@ -390,7 +390,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error {
awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws")
configPath := filepath.Join(awsConfigDir, "config")

configContent, err := os.ReadFile(configPath)
configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir
if err != nil {
return fmt.Errorf("failed to read AWS config: %w", err)
}
Expand Down Expand Up @@ -419,7 +419,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to marshal credentials: %w", err)
}

fmt.Fprintln(os.Stdout, string(jsonOutput))
fmt.Fprintln(os.Stdout, string(jsonOutput)) //nolint:errcheck // writing to stdout
return nil
}

Expand Down Expand Up @@ -469,7 +469,7 @@ func getSSOToken(ssoStartURL string) (string, error) {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") {
continue
}
content, err := os.ReadFile(filepath.Join(cacheDir, file.Name()))
content, err := os.ReadFile(filepath.Join(cacheDir, file.Name())) //nolint:gosec // path from known cache dir
if err != nil {
continue
}
Expand Down
132 changes: 132 additions & 0 deletions cmd/cone/install_mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"encoding/json"
"fmt"
"os/exec"
"strings"

"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

func installMCPCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "install-mcp",
Short: "Connect Claude Code to ConductorOne's hosted MCP gateway.",
Long: `Registers ConductorOne's hosted MCP endpoint with Claude Code.

Claude Code handles OAuth (including DCR) on first connection.
Cone just provides the endpoint URL based on your existing login.

Requires 'cone login <tenant>' to have been run first.`,
RunE: installMCPRun,
}

cmd.Flags().String("scope", "user", "Claude Code scope: user or project")
cmd.Flags().Bool("dry-run", false, "Print what would happen without doing it")
cmd.Flags().Bool("manual", false, "Print config snippet instead of running claude CLI")

return cmd
}

func installMCPRun(cmd *cobra.Command, _ []string) error {
v, err := getSubViperForProfile(cmd)
if err != nil {
return err
}

clientID := v.GetString("client-id")
if clientID == "" {
return fmt.Errorf("not authenticated. Run 'cone login <tenant>' first")
}

// Parse tenant host from client-id.
// Format: {name}@{host}/{path}
tenantHost, err := parseTenantHost(clientID)
if err != nil {
return fmt.Errorf("could not determine tenant from client-id %q: %w", clientID, err)
}

mcpURL := fmt.Sprintf("https://%s/api/v1alpha/mcp", tenantHost)

profile := v.GetString("profile")
if profile == "" {
profile = "default"
}
serverName := "conductorone"
if profile != "default" {
serverName = fmt.Sprintf("conductorone-%s", profile)
}

scope, _ := cmd.Flags().GetString("scope")
dryRun, _ := cmd.Flags().GetBool("dry-run")
manual, _ := cmd.Flags().GetBool("manual")

if dryRun {
pterm.Printf("Would run:\n claude mcp add --transport http --scope %s %s %s\n", scope, serverName, mcpURL)
return nil
}

if manual {
printManualMCPConfig(serverName, mcpURL)
return nil
}

claudePath, _ := exec.LookPath("claude")
if claudePath == "" {
pterm.Printf("claude CLI not found on PATH. Falling back to manual config.\n\n")
printManualMCPConfig(serverName, mcpURL)
return nil
}

spinner, err := pterm.DefaultSpinner.Start(fmt.Sprintf("Registering MCP server %q...", serverName))
if err != nil {
return err
}

out, err := exec.CommandContext(cmd.Context(), claudePath, "mcp", "add", //nolint:gosec // args are from config, not user input
"--transport", "http",
"--scope", scope,
serverName, mcpURL,
).CombinedOutput()
if err != nil {
spinner.Fail(fmt.Sprintf("Failed to register: %s", strings.TrimSpace(string(out))))
return fmt.Errorf("claude mcp add failed: %w", err)
}

spinner.Success(fmt.Sprintf("Registered %q (scope: %s)", serverName, scope))
pterm.Printf("\nEndpoint: %s\n", mcpURL)
pterm.Printf("Claude Code will handle OAuth on first connection.\n")
pterm.Printf("Restart Claude Code or run /mcp to connect.\n")

return nil
}

// parseTenantHost extracts the host from a C1 client-id.
// Client-id format: {name}@{host}/{path}.
func parseTenantHost(clientID string) (string, error) {
parts := strings.SplitN(clientID, "@", 2)
if len(parts) != 2 {
return "", fmt.Errorf("expected format {name}@{host}/{path}")
}
hostPath := parts[1]
hostParts := strings.SplitN(hostPath, "/", 2)
if len(hostParts) != 2 {
return "", fmt.Errorf("expected format {name}@{host}/{path}")
}
return hostParts[0], nil
}

func printManualMCPConfig(serverName, mcpURL string) {
config := map[string]any{
"type": "http",
"url": mcpURL,
}
configJSON, _ := json.MarshalIndent(map[string]any{
serverName: config,
}, "", " ")

pterm.Printf("Add the following to your Claude Code MCP config:\n\n")
pterm.Printf("%s\n", configJSON)
}
Loading
Loading