Skip to content

Conversation

@robert-chiniquy
Copy link
Contributor

@robert-chiniquy robert-chiniquy commented Jan 26, 2026

Add scaffolding command: init and also a publish command (depends upon registry-api or similar service)
Add connector subcommands for dev workflow and AI analysis: analyze, consent, dev, validate commands with supporting
packages for MCP client, consent storage, and prompts.

Summary by CodeRabbit

  • New Features

    • Persistent, versioned user consent management with re-consent support and human-readable consent text.
    • MCP client implementing JSON‑RPC analysis flow with iterative tool callbacks and result aggregation.
    • Connector-scoped tool handler for safe read/write/edit actions and interactive tools.
    • Terminal prompting utilities for interactive and non-interactive CLI flows.
    • Scaffold generator to produce connector projects and a mock MCP server for testing.
  • Bug Fixes

    • Improved entitlement handling to avoid nil dereferences.
  • Tests

    • Extensive unit, integration, and end-to-end tests covering consent, MCP client, tools, prompts, scaffold, and mock server.
  • Chores

    • Updated module dependency declarations.

✏️ Tip: You can customize this high-level summary in your review settings.

robert-chiniquy and others added 6 commits December 27, 2025 20:03
Adds `cone connector` command group with:
- `cone connector build [path]` - builds connector binaries
  - Supports cross-compilation via --os and --arch flags
  - Validates go.mod exists before building

Part of Tier 1 implementation (MASTER_PLAN.md feature [1]).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds pkg/scaffold package and `cone connector init <name>` command for
creating new connector projects from a standard template.

Generated project includes:
- main.go with CLI setup using baton-sdk
- config/config.go with field definitions
- pkg/connector/ with resource syncers (users, groups, roles)
- pkg/client/ with API client stub
- go.mod, Makefile, README.md, .gitignore

Features:
- --module flag for custom Go module path
- --description flag for connector description
- Auto-generates baton-<name> directory
- Template variables for name, module path, description

Part of Tier 2 implementation (MASTER_PLAN.md feature [6]).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Template was using obsolete SDK API that doesn't exist:
- cli.NewApp, cli.WithConnector (never existed)
- Wrong SDK version (v0.2.0 vs v0.4.7)

Changed to current patterns:
- configSdk.DefineConfiguration() for CLI setup
- field.NewConfiguration() for config schema
- field.Configurable interface implementation
- connectorbuilder.NewConnector() wrapper

Verified: generated connector compiles and runs.
  analyze, consent, dev, validate commands with supporting
  packages for MCP client, consent storage, and prompts.

The AI-related commands have only been tested against mocks until more
C1 work occurs
@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • cmd/cone/connector.go is excluded by none and included by none

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

Adds a versioned consent manager, a JSON‑RPC MCP client with tool‑callback handling and mock server, a file‑scoped ToolHandler, terminal prompting utilities, a connector scaffold generator, and extensive unit/integration tests across these components.

Changes

Cohort / File(s) Summary
Consent Management
pkg/consent/consent.go, pkg/consent/consent_test.go
New consent package: ConsentRecord, CurrentConsentVersion, ErrNoConsent, ErrConsentVersionMismatch; APIs Load, Save, Revoke, Status, ConsentText, HasValidConsent. Persists consent.json under ~/.cone with strict perms; comprehensive tests for perms, versioning, JSON edge cases.
MCP Client Core
pkg/mcpclient/client.go, pkg/mcpclient/client_test.go, pkg/mcpclient/integration_test.go
New JSON‑RPC Client and AnalysisResult; NewClient, Connect, Analyze, Close; analysis loop handling tool_call/tool_result/complete, auth header support, request sending and error handling; unit and integration tests.
Tool Execution
pkg/mcpclient/tools.go
ToolHandler (ConnectorDir, DryRun, Verbose) with HandleToolCall dispatcher and handlers for read_files, write_file, edit_file, ask_user, show_diff, confirm; enforces confinement to connector directory and returns structured ToolResult.
MCP Mock Server
pkg/mcpclient/mock/server.go, pkg/mcpclient/mock/server_test.go
Mock MCP server modeling Scenario/ToolCall, session tracking, handlers for initialize/tools/list/tools/call/tool_result, start/stop helpers, and tests for happy path and session/result collection.
MCP Integration Tests
pkg/mcpclient/integration_test.go
End‑to‑end integration tests exercising full analysis flows and raw protocol interactions with the mock server; helpers for JSON‑RPC request/response assertions.
Prompt Utilities
pkg/prompt/prompt.go, pkg/prompt/prompt_test.go
Terminal prompt utilities: Confirm/Input/Select/MultiSelect/DisplayBox, Option type, interactive guards and errors (ErrNotInteractive, ErrNoOptions, ErrCancelled), and extensive non‑interactive/edge‑case tests.
Project Scaffolding
pkg/scaffold/scaffold.go, pkg/scaffold/scaffold_test.go
Scaffold generator Generate(*Config) producing a Go connector project from templates, casing helpers, file generation with file modes; tests for generation, defaults, compile and vet checks.
Module file
go.mod
Dependency adjustments: added golang.org/x/term as a direct require; promoted github.com/fsnotify/fsnotify and gopkg.in/yaml.v3 to direct requires.
Minor client change
pkg/client/entitlement.go
Small internal view/path adjustments in entitlement handling; no exported API changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant ToolHandler

    Client->>Server: initialize (JSON‑RPC)
    Server-->>Client: initialize response
    Client->>Server: connector_analyze (JSON‑RPC)
    Server-->>Client: tool_call (e.g., read_files)
    Client->>ToolHandler: HandleToolCall(read_files)
    ToolHandler-->>Client: ToolResult (files/data)
    Client->>Server: tool_result (JSON‑RPC)
    Server-->>Client: tool_call (e.g., ask_user)
    Client->>ToolHandler: HandleToolCall(ask_user)
    ToolHandler-->>Client: ToolResult (user response)
    Client->>Server: tool_result
    Server-->>Client: complete (summary)
    Client->>Client: produce final AnalysisResult
Loading
sequenceDiagram
    participant User
    participant System
    participant Disk

    User->>System: Load()
    System->>Disk: Read ~/.cone/consent.json
    alt file missing
        Disk-->>System: ENOENT
        System-->>User: ErrNoConsent
    else file present
        Disk-->>System: ConsentRecord JSON
        alt version == CurrentConsentVersion
            System-->>User: Valid ConsentRecord (timestamp, version)
        else version mismatch
            System-->>User: ErrConsentVersionMismatch
        end
    end
    opt Save consent
        User->>System: Save()
        System->>Disk: Ensure ~/.cone (0700), write consent.json (0600)
        Disk-->>System: success
        System-->>User: OK
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I dug a burrow of code tonight,
consent tucked safe under moonlight.
Servers call tools, handlers fetch files,
scaffolds sprout projects in neat little piles.
Tests hop along — the build feels right.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title 'RFR: Rch/scaffold and publish' is vague and uses abbreviations that obscure the actual changes; it does not clearly convey the primary purpose of adding scaffolding, publishing, and connector workflow commands. Clarify the title to be more descriptive, such as 'Add connector scaffolding, publishing, and workflow commands' or similar, replacing unclear abbreviations like 'RFR' and 'Rch' with meaningful context.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rch/scaffold-and-publish

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@pkg/consent/consent_test.go`:
- Around line 3-9: Update the tests in consent_test.go to be Windows-safe by
setting USERPROFILE in addition to HOME when overriding the home dir (use
os.Setenv for "USERPROFILE") and by guarding POSIX permission assertions (checks
against file modes like 0700/0600) with runtime.GOOS != "windows" so they are
skipped on Windows; specifically modify the test setup that overrides the home
directory (where os.UserHomeDir is relied on) and wrap any permission assertions
in an if runtime.GOOS != "windows" block to keep tests hermetic across
platforms.

In `@pkg/mcpclient/client_test.go`:
- Around line 14-34: The tests currently ignore errors from json.Decode,
json.Encode and json.Marshal which trips errcheck; update each handler in
pkg/mcpclient/client_test.go to check and handle those errors (e.g., after
json.NewDecoder(r.Body).Decode(&req) and after json.NewEncoder(w).Encode(resp)
return t.Fatalf or t.Errorf with the error), and update the jsonBody helper to
capture json.Marshal error and return it (or t.Fatalf from callers) instead of
discarding it; specifically modify the anonymous httptest handlers (where
Decode/Encode are called) and the jsonBody function to assign the error to a
variable, check it, and fail the test with a clear message including the error.
- Around line 96-112: In the mock server handler in pkg/mcpclient/client_test.go
(the "tools/call" case) replace the direct type assertion params :=
req["params"].(map[string]interface{}) with a safe ok-check (e.g., params, ok :=
req["params"].(map[string]interface{})) and if !ok return a JSON-RPC error
response (using the same resp structure with "error" containing code/message and
the incoming id) so the test mock fails gracefully instead of panicking; update
subsequent references (params["name"]) to use the guarded params variable.

In `@pkg/mcpclient/client.go`:
- Around line 141-212: In processAnalysisResponse, guard against a nil
ToolHandler before calling c.ToolHandler.HandleToolCall: if c.ToolHandler == nil
return a clear error (e.g., fmt.Errorf("no ToolHandler configured for tool_call
%s", result.ToolCall.Name)) instead of attempting to call HandleToolCall; apply
this check in the "tool_call" case (referencing processAnalysisResponse,
result.ToolCall, and ToolHandler.HandleToolCall) so the function returns a
helpful error instead of panicking.
- Around line 214-260: The sendRequest method dereferences c.HTTPClient directly
which can panic when nil; change it to use a local httpClient variable: if
c.HTTPClient is nil, set httpClient = &http.Client{Timeout: c.Timeout} (so the
documented default client behavior and the Client.Timeout field are honored),
otherwise use the provided c.HTTPClient unchanged; then call httpClient.Do(...)
and adjust any uses of c.HTTPClient to httpClient in sendRequest.

In `@pkg/mcpclient/mock/server_test.go`:
- Around line 33-35: The json.Marshal calls currently ignore errors (e.g., where
listBytes, _ := json.Marshal(resp["result"]) and the other two occurrences)
which can mask failures; update each Marshal usage to capture the error, check
it, and fail the test with t.Fatalf including the error (e.g., replace the blank
identifier with err, and call t.Fatalf("json.Marshal failed for resp[...] : %v",
err) before proceeding to json.Unmarshal). Apply this pattern to the three
instances around variables listBytes/listResult and the analogous variables at
lines 58 and 121 so Marshal failures are surfaced immediately.

In `@pkg/mcpclient/mock/server.go`:
- Around line 225-246: The response helpers sendResult and sendError currently
ignore json.NewEncoder(w).Encode errors; update both functions to capture the
returned error from Encoder.Encode and handle it (e.g., log the encoding error
via the server logger or fmt.Errorf and then close the connection or return)
instead of discarding it; ensure the Content-Type header is still set before
encoding, but do not attempt to call http.Error after headers are written —
simply log the error with context (include the id and whether it was result or
error) and terminate writing/close the response to avoid silent failures.

In `@pkg/mcpclient/tools.go`:
- Around line 264-317: In handleWriteFile, avoid calling prompt.Confirm when
h.DryRun is true: after detecting the file exists (the os.Stat(fullPath) == nil
branch) short-circuit and return the dry-run result if h.DryRun is set instead
of calling prompt.Confirm; move or add the h.DryRun check before invoking
prompt.Confirm so non-interactive dry runs never block.
- Around line 196-262: The handler handleEditFile currently allows missing "new"
to default to empty string which can delete content; validate the "new" argument
from args before proceeding (similar to the existing check for "path" and "old")
by ensuring args["new"] is present and is a non-empty string, and return a
ToolResult with Success:false and a clear error message (e.g., "missing required
argument: new") if it isn't; update the early-argument validation block (where
path, old, new are read) to perform this check and abort before showing the diff
or writing the file.
- Around line 64-127: handleReadFiles (and similarly
handleEditFile/handleWriteFile) is vulnerable to symlink TOCTOU because it uses
filepath.Join/Rel plus os.Stat/os.ReadFile; instead, wrap h.ConnectorDir with
os.Root and open target files via the root's OpenInRoot to atomically resolve
and read files beneath the directory. Concretely: create an os.Root for
h.ConnectorDir, use the relative path (relPath) computed from the glob match (or
pattern) and call root.OpenInRoot(relPath) to get an *os.File, replace os.Stat
with f.Stat() and os.ReadFile with io.ReadAll(f) (closing f), and remove
string-path-based security checks—apply the same replacement in handleEditFile
and handleWriteFile so all file access is symlink-safe and TOCTOU-resistant.

In `@pkg/prompt/prompt.go`:
- Around line 272-328: The DisplayBox function can panic when a long title makes
padding negative; update DisplayBox to truncate title to at most width-2
characters (or width-4 if you want spaces around), compute padding from the
truncated title, and clamp padding to zero (e.g., if
(width-2-len(truncatedTitle)) / 2 < 0 then set padding = 0) before calling
strings.Repeat; reference the DisplayBox function and the local variables title
and padding so you replace uses of title with the truncatedTitle and ensure
padding is non-negative to avoid strings.Repeat panics.

In `@pkg/scaffold/scaffold_test.go`:
- Around line 99-101: Handle errors from os.Getwd() and os.Chdir(tmpDir) and
ensure cleanup uses t.Cleanup: call oldWd, err := os.Getwd() and if err != nil
use t.Fatalf to stop the test; then if os.Chdir(tmpDir) returns an error use
t.Fatalf; register t.Cleanup(func(){ if err := os.Chdir(oldWd); err != nil {
t.Fatalf("restore cwd: %v", err) } }) instead of defer so the original working
directory is reliably restored even if the test fails.

In `@pkg/scaffold/scaffold.go`:
- Around line 24-31: Add a nil check for the incoming *Config at the top of
Generate and validate cfg.Name before using it: return an error if cfg is nil;
ensure cfg.Name matches a safe pattern (only lowercase letters, digits, and
hyphens, e.g. regex ^[a-z0-9-]+$), reject names containing "..",
leading/trailing slashes, or any path separators to prevent path traversal, and
return a clear error if invalid; only after validation, set cfg.ModulePath (when
empty) using the validated name to form "github.com/conductorone/baton-<name>";
ensure any later use of cfg.Name with filepath.Join or file writes relies on the
validated value to avoid writing outside the intended directory (references:
Generate, Config, cfg.Name, cfg.ModulePath, filepath.Join).
🧹 Nitpick comments (5)
pkg/scaffold/scaffold_test.go (1)

192-193: Use os.DevNull for cross-platform build output.

Line 193 hardcodes /dev/null, which fails on Windows. os.DevNull keeps this test portable.

♻️ Suggested change
-	runCommand(t, outputDir, 3*time.Minute, "go", "build", "-o", "/dev/null", ".")
+	runCommand(t, outputDir, 3*time.Minute, "go", "build", "-o", os.DevNull, ".")
pkg/scaffold/scaffold.go (1)

64-66: Fail fast on missing template keys.

Line 64-66 uses the default text/template missing-key behavior, which silently renders empty strings. Enabling missingkey=error catches template drift early (especially for docs that compile tests won’t catch).

♻️ Suggested change
-	tmpl, err := template.New(tf.Path).Parse(tf.Template)
+	tmpl, err := template.New(tf.Path).
+		Option("missingkey=error").
+		Parse(tf.Template)
pkg/consent/consent.go (1)

115-140: Optionally reapply 0600 when overwriting consent.json.

os.WriteFile only applies permissions on create; if the file already exists with broader perms, they remain. If you want the 0600 guarantee to always hold, follow with os.Chmod.

♻️ Proposed hardening
  // Write with restrictive permissions (0600 = rw-------)
  if err := os.WriteFile(path, data, 0600); err != nil {
    return fmt.Errorf("failed to write consent file: %w", err)
  }
+ if err := os.Chmod(path, 0600); err != nil {
+   return fmt.Errorf("failed to set consent file permissions: %w", err)
+ }
pkg/mcpclient/client_test.go (1)

143-150: Strengthen the assertion in the Analyze test.

The test currently passes even if Analyze unexpectedly succeeds. Consider asserting the expected error (or expected success) to make the test deterministic.

pkg/mcpclient/mock/server_test.go (1)

92-100: Consider asserting tool_result responses.

To make the test deterministic, capture each doRequest response and call assertNoError before moving on.

Comment on lines 3 to 9
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n pkg/consent/consent_test.go | head -50

Repository: ConductorOne/cone

Length of output: 1445


🏁 Script executed:

wc -l pkg/consent/consent_test.go

Repository: ConductorOne/cone

Length of output: 92


🏁 Script executed:

sed -n '60,80p' pkg/consent/consent_test.go

Repository: ConductorOne/cone

Length of output: 511


🏁 Script executed:

sed -n '230,250p' pkg/consent/consent_test.go

Repository: ConductorOne/cone

Length of output: 554


🏁 Script executed:

sed -n '390,410p' pkg/consent/consent_test.go

Repository: ConductorOne/cone

Length of output: 634


🏁 Script executed:

sed -n '1,50p' pkg/consent/consent.go

Repository: ConductorOne/cone

Length of output: 1783


🏁 Script executed:

cat pkg/consent/consent.go | grep -A 5 "func ensureConsentDir"

Repository: ConductorOne/cone

Length of output: 152


🏁 Script executed:

sed -n '50,100p' pkg/consent/consent.go

Repository: ConductorOne/cone

Length of output: 1457


🌐 Web query:

os.UserHomeDir Windows HOME environment variable behavior

💡 Result:

In Go, os.UserHomeDir() does not use the HOME environment variable on Windows.

  • Unix/macOS: returns $HOME.
  • Windows: returns %USERPROFILE% (the user profile directory, typically C:\Users\<name>). If the expected variable isn’t set, it may return a platform default or an error. [1]
  • Windows commonly exposes the user profile via %USERPROFILE% (and related variables like %HOMEPATH%). [2]

So on Windows, setting HOME (e.g., via Git Bash/MSYS/Cygwin) generally won’t affect os.UserHomeDir(); if you want to honor HOME on Windows, you must check os.Getenv("HOME") yourself before falling back to os.UserHomeDir().

Sources: [1] Go os.UserHomeDir docs (pkg.go.dev) (pkg.go.dev), [2] Microsoft Learn: recognized environment variables (USERPROFILE, HOMEPATH) (learn.microsoft.com)

Citations:


🏁 Script executed:

grep -n "runtime.GOOS\|build.*windows\|+build" pkg/consent/consent_test.go pkg/consent/consent.go

Repository: ConductorOne/cone

Length of output: 43


🏁 Script executed:

cat go.mod | head -20

Repository: ConductorOne/cone

Length of output: 492


🏁 Script executed:

cat -n .github/workflows/*.yml 2>/dev/null | grep -i "windows\|os:" | head -20

Repository: ConductorOne/cone

Length of output: 43


🏁 Script executed:

find .github -name "*.yml" -o -name "*.yaml" | xargs cat 2>/dev/null

Repository: ConductorOne/cone

Length of output: 6182


Make HOME override and permission checks Windows-safe.

os.UserHomeDir ignores HOME on Windows and reads USERPROFILE instead, so tests that only set HOME will fail there. Additionally, POSIX permission checks (0700, 0600) are unreliable on Windows. If Windows support is planned, set USERPROFILE in test setup and guard permission assertions with runtime.GOOS != "windows" to keep tests hermetic across platforms.

🛠️ Proposed fix
 import (
 	"encoding/json"
 	"os"
 	"path/filepath"
+	"runtime"
 	"testing"
 	"time"
 )

 // setupTestDir creates a temp directory and sets HOME to point to it.
 // Returns a cleanup function that restores the original HOME.
 func setupTestDir(t *testing.T) func() {
 	t.Helper()

 	originalHome := os.Getenv("HOME")
+	originalUserProfile := os.Getenv("USERPROFILE")
 	tmpDir := t.TempDir()
 	if err := os.Setenv("HOME", tmpDir); err != nil {
 		t.Fatalf("failed to set HOME: %v", err)
 	}
+	_ = os.Setenv("USERPROFILE", tmpDir)

 	return func() {
-		os.Setenv("HOME", originalHome)
+		_ = os.Setenv("HOME", originalHome)
+		_ = os.Setenv("USERPROFILE", originalUserProfile)
 	}
 }
@@
-	if info.Mode().Perm() != 0700 {
+	if runtime.GOOS != "windows" && info.Mode().Perm() != 0700 {
 		t.Errorf("expected permissions 0700, got %o", info.Mode().Perm())
 	}
@@
-	if info.Mode().Perm() != 0600 {
+	if runtime.GOOS != "windows" && info.Mode().Perm() != 0600 {
 		t.Errorf("expected permissions 0600, got %o", info.Mode().Perm())
 	}
@@
-	if info.Mode().Perm() != 0700 {
+	if runtime.GOOS != "windows" && info.Mode().Perm() != 0700 {
 		t.Errorf("expected permissions 0700, got %o", info.Mode().Perm())
 	}

Also applies to: 16-24, 69-77, 239-245, 399-405

🤖 Prompt for AI Agents
In `@pkg/consent/consent_test.go` around lines 3 - 9, Update the tests in
consent_test.go to be Windows-safe by setting USERPROFILE in addition to HOME
when overriding the home dir (use os.Setenv for "USERPROFILE") and by guarding
POSIX permission assertions (checks against file modes like 0700/0600) with
runtime.GOOS != "windows" so they are skipped on Windows; specifically modify
the test setup that overrides the home directory (where os.UserHomeDir is relied
on) and wrap any permission assertions in an if runtime.GOOS != "windows" block
to keep tests hermetic across platforms.

Comment on lines 14 to 34
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)

if req["method"] != "initialize" {
t.Errorf("expected initialize method, got %v", req["method"])
}

resp := map[string]interface{}{
"jsonrpc": "2.0",
"id": req["id"],
"result": map[string]interface{}{
"protocolVersion": "2024-11-05",
"serverInfo": map[string]string{
"name": "test-server",
"version": "1.0",
},
},
}
json.NewEncoder(w).Encode(resp)
}))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "client_test.go" -type f | head -20

Repository: ConductorOne/cone

Length of output: 91


🏁 Script executed:

cat -n ./pkg/mcpclient/client_test.go

Repository: ConductorOne/cone

Length of output: 7670


🏁 Script executed:

# Check for errcheck configuration in the repository
find . -name ".golangci.yml" -o -name ".golangci.yaml" -o -name "golangci.yml" | head -5
cat .golangci.yml 2>/dev/null || cat .golangci.yaml 2>/dev/null || echo "No golangci config found"

Repository: ConductorOne/cone

Length of output: 5105


🏁 Script executed:

git log --oneline --all | head -20
git diff --stat HEAD~5..HEAD 2>/dev/null | head -20

Repository: ConductorOne/cone

Length of output: 165


🏁 Script executed:

# Check for CI configuration files
find . -name "*.yml" -o -name "*.yaml" | grep -E "(github|gitlab|circle|travis|azure)" | head -10
ls -la .github/workflows/ 2>/dev/null || echo "No workflows directory"

Repository: ConductorOne/cone

Length of output: 885


🏁 Script executed:

cat .github/workflows/ci.yaml

Repository: ConductorOne/cone

Length of output: 1041


Handle JSON Encode/Decode errors to fix errcheck failures.

errcheck is failing on unhandled Decode, Encode, and Marshal errors across the test file. These must be fixed to keep CI green.

🐛 Suggested pattern (apply similarly in each handler and in jsonBody)
-			json.NewDecoder(r.Body).Decode(&req)
+			if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+				t.Errorf("failed to decode request: %v", err)
+				http.Error(w, "bad request", http.StatusBadRequest)
+				return
+			}
-			json.NewEncoder(w).Encode(resp)
+			if err := json.NewEncoder(w).Encode(resp); err != nil {
+				t.Errorf("failed to encode response: %v", err)
+			}
-	body, _ := json.Marshal(req)
-	return bytes.NewBuffer(body)
+	body, err := json.Marshal(req)
+	if err != nil {
+		panic(err)
+	}
+	return bytes.NewBuffer(body)

Locations: Lines 16, 33, 51, 61, 82, 127 (Decode/Encode in handlers), and line 224 (Marshal in jsonBody helper).

🧰 Tools
🪛 GitHub Check: go-lint

[failure] 33-33:
Error return value of (*encoding/json.Encoder).Encode is not checked (errcheck)


[failure] 16-16:
Error return value of (*encoding/json.Decoder).Decode is not checked (errcheck)

🤖 Prompt for AI Agents
In `@pkg/mcpclient/client_test.go` around lines 14 - 34, The tests currently
ignore errors from json.Decode, json.Encode and json.Marshal which trips
errcheck; update each handler in pkg/mcpclient/client_test.go to check and
handle those errors (e.g., after json.NewDecoder(r.Body).Decode(&req) and after
json.NewEncoder(w).Encode(resp) return t.Fatalf or t.Errorf with the error), and
update the jsonBody helper to capture json.Marshal error and return it (or
t.Fatalf from callers) instead of discarding it; specifically modify the
anonymous httptest handlers (where Decode/Encode are called) and the jsonBody
function to assign the error to a variable, check it, and fail the test with a
clear message including the error.

Comment on lines 96 to 112
case "tools/call":
params := req["params"].(map[string]interface{})
if params["name"] == "connector_analyze" {
resp = map[string]interface{}{
"jsonrpc": "2.0",
"id": req["id"],
"result": map[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": `{"status":"tool_call","session_id":"test-session","tool_call":{"name":"read_files","arguments":{"paths":["go.mod"]}}}`,
},
},
},
}
} else if params["name"] == "tool_result" {
resp = map[string]interface{}{
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check its size
ls -la pkg/mcpclient/client_test.go 2>/dev/null || echo "File not found at expected location"
wc -l pkg/mcpclient/client_test.go 2>/dev/null || echo "Cannot count lines"

Repository: ConductorOne/cone

Length of output: 171


🏁 Script executed:

# Read the specific lines mentioned in the review (96-112)
sed -n '80,130p' pkg/mcpclient/client_test.go

Repository: ConductorOne/cone

Length of output: 1466


🏁 Script executed:

# Search for other type assertions in the same test file to understand the pattern
rg '\.\(map\[string\]' pkg/mcpclient/client_test.go -n -A 2 -B 2

Repository: ConductorOne/cone

Length of output: 239


🏁 Script executed:

# Look at the complete switch statement and surrounding code to understand context
sed -n '80,150p' pkg/mcpclient/client_test.go

Repository: ConductorOne/cone

Length of output: 2111


🏁 Script executed:

# Check if there are other similar type assertions without ok-checks in the same pattern
rg 'req\[' pkg/mcpclient/client_test.go -n -B 1 -A 1 | head -40

Repository: ConductorOne/cone

Length of output: 950


🏁 Script executed:

# Look for other test handlers or mock servers in this file or related files to see the pattern
rg 'httptest\.|switch.*method' pkg/mcpclient/ -n -A 5

Repository: ConductorOne/cone

Length of output: 3353


🏁 Script executed:

# Check the integration_test.go mock server to see if it has similar patterns
sed -n '1,150p' pkg/mcpclient/integration_test.go | grep -A 30 "HandleMCP\|switch"

Repository: ConductorOne/cone

Length of output: 1786


🏁 Script executed:

# Look at the mock/server.go to see how a more complete mock handles this
cat pkg/mcpclient/mock/server.go | head -100

Repository: ConductorOne/cone

Length of output: 2486


🏁 Script executed:

# Search for type assertions with ok-checks in test files
rg '\.\(map\[string\].*\)\,\s*ok' pkg/mcpclient/ -n

Repository: ConductorOne/cone

Length of output: 176


🏁 Script executed:

# Search for type assertions with ok-checks (proper escaping)
rg 'ok :=' pkg/mcpclient/ -n -B 2 -A 2 | head -50

Repository: ConductorOne/cone

Length of output: 2216


🏁 Script executed:

# Look at the full test function context to understand what requests are being made
sed -n '75,135p' pkg/mcpclient/client_test.go

Repository: ConductorOne/cone

Length of output: 1727


🏁 Script executed:

# Check what the client code sends when calling the server
rg -A 10 'tools/call\|Analyze' pkg/mcpclient/client.go | head -40

Repository: ConductorOne/cone

Length of output: 43


🏁 Script executed:

# Look at the newRequest function to see how requests are constructed
rg -B 5 -A 15 'func.*newRequest\|func.*buildRequest' pkg/mcpclient/client.go

Repository: ConductorOne/cone

Length of output: 43


🏁 Script executed:

# Find how the client calls tools/call
rg 'tools/call' pkg/mcpclient/ -n -B 5 -A 10

Repository: ConductorOne/cone

Length of output: 7435


🏁 Script executed:

# Look at the Client struct and its methods
rg -A 20 'func.*\(c \*Client\)' pkg/mcpclient/client.go | head -60

Repository: ConductorOne/cone

Length of output: 1817


🏁 Script executed:

# Check if the mock server or test sets up what params should be
sed -n '1,80p' pkg/mcpclient/client_test.go

Repository: ConductorOne/cone

Length of output: 1991


Consider guarding the params type assertion in the mock server handler.

While this test mock server controls its own inputs, a direct type assertion can still panic if the code path changes. Since the handler processes potentially variable request shapes, use an ok-check to return a JSON-RPC error response for robustness:

🛠️ Suggested guard
			case "tools/call":
-				params := req["params"].(map[string]interface{})
+				params, ok := req["params"].(map[string]interface{})
+				if !ok {
+					resp = map[string]interface{}{
+						"jsonrpc": "2.0",
+						"id":      req["id"],
+						"error": map[string]interface{}{
+							"code":    -32602,
+							"message": "Invalid params",
+						},
+					}
+					break
+				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "tools/call":
params := req["params"].(map[string]interface{})
if params["name"] == "connector_analyze" {
resp = map[string]interface{}{
"jsonrpc": "2.0",
"id": req["id"],
"result": map[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": `{"status":"tool_call","session_id":"test-session","tool_call":{"name":"read_files","arguments":{"paths":["go.mod"]}}}`,
},
},
},
}
} else if params["name"] == "tool_result" {
resp = map[string]interface{}{
case "tools/call":
params, ok := req["params"].(map[string]interface{})
if !ok {
resp = map[string]interface{}{
"jsonrpc": "2.0",
"id": req["id"],
"error": map[string]interface{}{
"code": -32602,
"message": "Invalid params",
},
}
break
}
if params["name"] == "connector_analyze" {
resp = map[string]interface{}{
"jsonrpc": "2.0",
"id": req["id"],
"result": map[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": `{"status":"tool_call","session_id":"test-session","tool_call":{"name":"read_files","arguments":{"paths":["go.mod"]}}}`,
},
},
},
}
} else if params["name"] == "tool_result" {
resp = map[string]interface{}{
🤖 Prompt for AI Agents
In `@pkg/mcpclient/client_test.go` around lines 96 - 112, In the mock server
handler in pkg/mcpclient/client_test.go (the "tools/call" case) replace the
direct type assertion params := req["params"].(map[string]interface{}) with a
safe ok-check (e.g., params, ok := req["params"].(map[string]interface{})) and
if !ok return a JSON-RPC error response (using the same resp structure with
"error" containing code/message and the incoming id) so the test mock fails
gracefully instead of panicking; update subsequent references (params["name"])
to use the guarded params variable.

Comment on lines 196 to 262
// handleEditFile applies an edit to a file.
// Security: Only edits files within ConnectorDir.
func (h *ToolHandler) handleEditFile(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
path, _ := args["path"].(string)
old, _ := args["old"].(string)
new, _ := args["new"].(string)

if path == "" || old == "" {
return &ToolResult{Success: false, Error: "missing required arguments (path, old)"}, nil
}

// Security: Resolve path relative to connector directory
fullPath := filepath.Join(h.ConnectorDir, path)
relPath, err := filepath.Rel(h.ConnectorDir, fullPath)
if err != nil || strings.HasPrefix(relPath, "..") {
return &ToolResult{Success: false, Error: "path must be within connector directory"}, nil
}

// Read current content
content, err := os.ReadFile(fullPath)
if err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to read file: %v", err)}, nil
}

// Check if old string exists
if !strings.Contains(string(content), old) {
return &ToolResult{Success: false, Error: "old string not found in file"}, nil
}

// Show diff and ask for confirmation
fmt.Printf("\n--- %s (before)\n", path)
fmt.Printf("+++ %s (after)\n", path)
fmt.Printf("@@ edit @@\n")
fmt.Printf("-%s\n", strings.ReplaceAll(old, "\n", "\n-"))
fmt.Printf("+%s\n", strings.ReplaceAll(new, "\n", "\n+"))
fmt.Println()

if h.DryRun {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "dry run"},
}, nil
}

accepted, err := prompt.Confirm("Apply this change?")
if err != nil {
return &ToolResult{Success: false, Error: err.Error()}, nil
}

if !accepted {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "user declined"},
}, nil
}

// Apply the edit
newContent := strings.Replace(string(content), old, new, 1)
if err := os.WriteFile(fullPath, []byte(newContent), 0644); err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil
}

return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": true},
}, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require explicit new argument before editing.
If new is missing, it defaults to "" and can unintentionally delete content. Validate presence/type explicitly.

🐛 Proposed fix
 	path, _ := args["path"].(string)
 	old, _ := args["old"].(string)
-	new, _ := args["new"].(string)
+	newRaw, ok := args["new"]
+	if !ok {
+		return &ToolResult{Success: false, Error: "missing required arguments (path, old, new)"}, nil
+	}
+	new, ok := newRaw.(string)
+	if !ok {
+		return &ToolResult{Success: false, Error: "'new' must be a string"}, nil
+	}
 
 	if path == "" || old == "" {
 		return &ToolResult{Success: false, Error: "missing required arguments (path, old)"}, nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// handleEditFile applies an edit to a file.
// Security: Only edits files within ConnectorDir.
func (h *ToolHandler) handleEditFile(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
path, _ := args["path"].(string)
old, _ := args["old"].(string)
new, _ := args["new"].(string)
if path == "" || old == "" {
return &ToolResult{Success: false, Error: "missing required arguments (path, old)"}, nil
}
// Security: Resolve path relative to connector directory
fullPath := filepath.Join(h.ConnectorDir, path)
relPath, err := filepath.Rel(h.ConnectorDir, fullPath)
if err != nil || strings.HasPrefix(relPath, "..") {
return &ToolResult{Success: false, Error: "path must be within connector directory"}, nil
}
// Read current content
content, err := os.ReadFile(fullPath)
if err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to read file: %v", err)}, nil
}
// Check if old string exists
if !strings.Contains(string(content), old) {
return &ToolResult{Success: false, Error: "old string not found in file"}, nil
}
// Show diff and ask for confirmation
fmt.Printf("\n--- %s (before)\n", path)
fmt.Printf("+++ %s (after)\n", path)
fmt.Printf("@@ edit @@\n")
fmt.Printf("-%s\n", strings.ReplaceAll(old, "\n", "\n-"))
fmt.Printf("+%s\n", strings.ReplaceAll(new, "\n", "\n+"))
fmt.Println()
if h.DryRun {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "dry run"},
}, nil
}
accepted, err := prompt.Confirm("Apply this change?")
if err != nil {
return &ToolResult{Success: false, Error: err.Error()}, nil
}
if !accepted {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "user declined"},
}, nil
}
// Apply the edit
newContent := strings.Replace(string(content), old, new, 1)
if err := os.WriteFile(fullPath, []byte(newContent), 0644); err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil
}
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": true},
}, nil
}
// handleEditFile applies an edit to a file.
// Security: Only edits files within ConnectorDir.
func (h *ToolHandler) handleEditFile(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
path, _ := args["path"].(string)
old, _ := args["old"].(string)
newRaw, ok := args["new"]
if !ok {
return &ToolResult{Success: false, Error: "missing required arguments (path, old, new)"}, nil
}
new, ok := newRaw.(string)
if !ok {
return &ToolResult{Success: false, Error: "'new' must be a string"}, nil
}
if path == "" || old == "" {
return &ToolResult{Success: false, Error: "missing required arguments (path, old)"}, nil
}
// Security: Resolve path relative to connector directory
fullPath := filepath.Join(h.ConnectorDir, path)
relPath, err := filepath.Rel(h.ConnectorDir, fullPath)
if err != nil || strings.HasPrefix(relPath, "..") {
return &ToolResult{Success: false, Error: "path must be within connector directory"}, nil
}
// Read current content
content, err := os.ReadFile(fullPath)
if err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to read file: %v", err)}, nil
}
// Check if old string exists
if !strings.Contains(string(content), old) {
return &ToolResult{Success: false, Error: "old string not found in file"}, nil
}
// Show diff and ask for confirmation
fmt.Printf("\n--- %s (before)\n", path)
fmt.Printf("+++ %s (after)\n", path)
fmt.Printf("@@ edit @@\n")
fmt.Printf("-%s\n", strings.ReplaceAll(old, "\n", "\n-"))
fmt.Printf("+%s\n", strings.ReplaceAll(new, "\n", "\n+"))
fmt.Println()
if h.DryRun {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "dry run"},
}, nil
}
accepted, err := prompt.Confirm("Apply this change?")
if err != nil {
return &ToolResult{Success: false, Error: err.Error()}, nil
}
if !accepted {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": false, "reason": "user declined"},
}, nil
}
// Apply the edit
newContent := strings.Replace(string(content), old, new, 1)
if err := os.WriteFile(fullPath, []byte(newContent), 0644); err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil
}
return &ToolResult{
Success: true,
Data: map[string]interface{}{"applied": true},
}, nil
}
🤖 Prompt for AI Agents
In `@pkg/mcpclient/tools.go` around lines 196 - 262, The handler handleEditFile
currently allows missing "new" to default to empty string which can delete
content; validate the "new" argument from args before proceeding (similar to the
existing check for "path" and "old") by ensuring args["new"] is present and is a
non-empty string, and return a ToolResult with Success:false and a clear error
message (e.g., "missing required argument: new") if it isn't; update the
early-argument validation block (where path, old, new are read) to perform this
check and abort before showing the diff or writing the file.

Comment on lines 264 to 317
// handleWriteFile writes a new file.
// Security: Only writes files within ConnectorDir.
func (h *ToolHandler) handleWriteFile(ctx context.Context, args map[string]interface{}) (*ToolResult, error) {
path, _ := args["path"].(string)
content, _ := args["content"].(string)

if path == "" {
return &ToolResult{Success: false, Error: "missing 'path' argument"}, nil
}

// Security: Resolve path relative to connector directory
fullPath := filepath.Join(h.ConnectorDir, path)
relPath, err := filepath.Rel(h.ConnectorDir, fullPath)
if err != nil || strings.HasPrefix(relPath, "..") {
return &ToolResult{Success: false, Error: "path must be within connector directory"}, nil
}

// Check if file exists
if _, err := os.Stat(fullPath); err == nil {
// File exists, ask for confirmation
overwrite, err := prompt.Confirm(fmt.Sprintf("File %s exists. Overwrite?", path))
if err != nil {
return &ToolResult{Success: false, Error: err.Error()}, nil
}
if !overwrite {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"written": false, "reason": "user declined overwrite"},
}, nil
}
}

if h.DryRun {
return &ToolResult{
Success: true,
Data: map[string]interface{}{"written": false, "reason": "dry run"},
}, nil
}

// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to create directory: %v", err)}, nil
}

// Write the file
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
return &ToolResult{Success: false, Error: fmt.Sprintf("failed to write file: %v", err)}, nil
}

return &ToolResult{
Success: true,
Data: map[string]interface{}{"written": true},
}, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DryRun should bypass overwrite prompts.
In dry-run mode, prompting for overwrite can still block or fail in non-interactive runs. Short-circuit before prompting.

🐛 Proposed fix
 	relPath, err := filepath.Rel(h.ConnectorDir, fullPath)
 	if err != nil || strings.HasPrefix(relPath, "..") {
 		return &ToolResult{Success: false, Error: "path must be within connector directory"}, nil
 	}
 
+	if h.DryRun {
+		return &ToolResult{
+			Success: true,
+			Data:    map[string]interface{}{"written": false, "reason": "dry run"},
+		}, nil
+	}
+
 	// Check if file exists
 	if _, err := os.Stat(fullPath); err == nil {
 		// File exists, ask for confirmation
@@
-	if h.DryRun {
-		return &ToolResult{
-			Success: true,
-			Data:    map[string]interface{}{"written": false, "reason": "dry run"},
-		}, nil
-	}
-
 	// Ensure parent directory exists
 	if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
 		return &ToolResult{Success: false, Error: fmt.Sprintf("failed to create directory: %v", err)}, nil
 	}
🤖 Prompt for AI Agents
In `@pkg/mcpclient/tools.go` around lines 264 - 317, In handleWriteFile, avoid
calling prompt.Confirm when h.DryRun is true: after detecting the file exists
(the os.Stat(fullPath) == nil branch) short-circuit and return the dry-run
result if h.DryRun is set instead of calling prompt.Confirm; move or add the
h.DryRun check before invoking prompt.Confirm so non-interactive dry runs never
block.

Comment on lines 99 to 101
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd pkg/scaffold && wc -l scaffold_test.go

Repository: ConductorOne/cone

Length of output: 81


🏁 Script executed:

cd pkg/scaffold && sed -n '95,110p' scaffold_test.go

Repository: ConductorOne/cone

Length of output: 334


🏁 Script executed:

cd pkg/scaffold && sed -n '190,200p' scaffold_test.go

Repository: ConductorOne/cone

Length of output: 456


🏁 Script executed:

rg "Getwd|Chdir" pkg/scaffold/scaffold_test.go -B 2 -A 2

Repository: ConductorOne/cone

Length of output: 195


Add error handling for os.Getwd and os.Chdir to prevent test failures.

Lines 99-101 ignore errors from os.Getwd() and os.Chdir(). If these calls fail, the test runs in the wrong directory and cleanup may not restore the original state. Use error checks and t.Cleanup() to ensure reliable test isolation:

Suggested fix
-	oldWd, _ := os.Getwd()
-	os.Chdir(tmpDir)
-	defer os.Chdir(oldWd)
+	oldWd, err := os.Getwd()
+	if err != nil {
+		t.Fatalf("failed to get working dir: %v", err)
+	}
+	if err := os.Chdir(tmpDir); err != nil {
+		t.Fatalf("failed to chdir to temp dir: %v", err)
+	}
+	t.Cleanup(func() {
+		if err := os.Chdir(oldWd); err != nil {
+			t.Errorf("failed to restore working dir: %v", err)
+		}
+	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
oldWd, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(oldWd)
oldWd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working dir: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("failed to chdir to temp dir: %v", err)
}
t.Cleanup(func() {
if err := os.Chdir(oldWd); err != nil {
t.Errorf("failed to restore working dir: %v", err)
}
})
🧰 Tools
🪛 GitHub Check: go-lint

[failure] 101-101:
Error return value of os.Chdir is not checked (errcheck)


[failure] 100-100:
Error return value of os.Chdir is not checked (errcheck)

🤖 Prompt for AI Agents
In `@pkg/scaffold/scaffold_test.go` around lines 99 - 101, Handle errors from
os.Getwd() and os.Chdir(tmpDir) and ensure cleanup uses t.Cleanup: call oldWd,
err := os.Getwd() and if err != nil use t.Fatalf to stop the test; then if
os.Chdir(tmpDir) returns an error use t.Fatalf; register t.Cleanup(func(){ if
err := os.Chdir(oldWd); err != nil { t.Fatalf("restore cwd: %v", err) } })
instead of defer so the original working directory is reliably restored even if
the test fails.

Comment on lines +24 to +31
// Generate creates a new connector project from the standard template.
func Generate(cfg *Config) error {
if cfg.Name == "" {
return fmt.Errorf("scaffold: connector name is required")
}
if cfg.ModulePath == "" {
cfg.ModulePath = fmt.Sprintf("github.com/conductorone/baton-%s", cfg.Name)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the scaffold.go file to see the full context
cat -n pkg/scaffold/scaffold.go | head -100

Repository: ConductorOne/cone

Length of output: 3491


🏁 Script executed:

# Search for the generateFile function mentioned in the scratchpad
rg -n "func generateFile" pkg/scaffold/

Repository: ConductorOne/cone

Length of output: 144


🏁 Script executed:

# Look for all usages of cfg.Name to understand how it's used
rg -n "cfg\.Name" pkg/scaffold/

Repository: ConductorOne/cone

Length of output: 685


🏁 Script executed:

# Check if there are Config struct definitions and existing validations
rg -n "type Config struct" pkg/scaffold/ -A 10

Repository: ConductorOne/cone

Length of output: 1529


🏁 Script executed:

# Check if there are other callers of Generate to see if nil check is important
rg -n "Generate\(" pkg/scaffold/ -B 2 -A 2

Repository: ConductorOne/cone

Length of output: 1975


🏁 Script executed:

# Check tests to see how Generate is called
find . -name "*test.go" -type f | head -5 | xargs rg -l "Generate"

Repository: ConductorOne/cone

Length of output: 45


Add nil check and validate connector name to prevent panics and path traversal.

The Generate function accesses cfg.Name on line 26 without first checking if cfg is nil, which will panic. Additionally, cfg.Name is used to construct file paths (via filepath.Join on line 73) without validation. This allows path traversal attacks—if a user passes a name like "../../etc", it could write files outside the intended output directory. Also, invalid characters in the name can produce malformed Go module paths (line 30). Validate that cfg is non-nil and that the name contains only lowercase letters, digits, and hyphens (as indicated by the Config documentation example of "my-app").

🔒 Suggested fix
 func Generate(cfg *Config) error {
+	if cfg == nil {
+		return fmt.Errorf("scaffold: config is required")
+	}
 	if cfg.Name == "" {
 		return fmt.Errorf("scaffold: connector name is required")
 	}
+	for _, r := range cfg.Name {
+		if !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-') {
+			return fmt.Errorf("scaffold: invalid connector name %q", cfg.Name)
+		}
+	}
 	if cfg.ModulePath == "" {
 		cfg.ModulePath = fmt.Sprintf("github.com/conductorone/baton-%s", cfg.Name)
 	}
🤖 Prompt for AI Agents
In `@pkg/scaffold/scaffold.go` around lines 24 - 31, Add a nil check for the
incoming *Config at the top of Generate and validate cfg.Name before using it:
return an error if cfg is nil; ensure cfg.Name matches a safe pattern (only
lowercase letters, digits, and hyphens, e.g. regex ^[a-z0-9-]+$), reject names
containing "..", leading/trailing slashes, or any path separators to prevent
path traversal, and return a clear error if invalid; only after validation, set
cfg.ModulePath (when empty) using the validated name to form
"github.com/conductorone/baton-<name>"; ensure any later use of cfg.Name with
filepath.Join or file writes relies on the validated value to avoid writing
outside the intended directory (references: Generate, Config, cfg.Name,
cfg.ModulePath, filepath.Join).

- errcheck: handle ignored error returns (os.Chdir, json.Encode, etc.)
- gosec: add ReadHeaderTimeout, use 0600 file permissions
- errorlint: use errors.Is instead of direct comparison
- predeclared: rename old/new to avoid shadowing
- goconst: add statusToolCall constant
- goimports: fix import ordering
- godot: end comments with periods
- gocritic: convert if-else chains to switches
- tenv: use t.Setenv instead of os.Setenv in tests
- noctx: use http.NewRequestWithContext
- unused: remove dead code
- scaffold: remove unused imports from generated templates
- .golangci.yml: add exclusion rules for legitimate fmt.Print
  usage in CLI code and intentional nil error returns
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/prompt/prompt.go`:
- Around line 288-292: The loop printing boxed lines can panic when wrapText
returns an unwrappable long token because padding (calculated in the loop
iterating over lines) can be negative; update the logic in the for loop that
processes lines from wrapText so you clamp padding to a minimum of 0 before
using strings.Repeat (or skip repeating when negative), e.g., compute padding :=
width-2-len(line) and set padding = 0 if it's negative, then call fmt.Printf("|
%s%s|\n", line, strings.Repeat(" ", padding-1 >= 0 ? padding-1 : 0)); ensure the
change is applied to the block handling lines returned by wrapText so extremely
long words/URLs no longer cause a panic.
🧹 Nitpick comments (5)
pkg/consent/consent_test.go (1)

513-526: Consider using strings.Contains from the standard library.

These custom helper functions duplicate functionality already available in the strings package. Using strings.Contains(s, substr) would simplify the code and eliminate potential edge-case bugs.

♻️ Suggested refactor
-// contains is a helper for string containment checks.
-func contains(s, substr string) bool {
-	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
-		(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
-}
-
-func findSubstring(s, substr string) bool {
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
-}

Then replace all contains(...) calls with strings.Contains(...) and add "strings" to the imports.

pkg/mcpclient/integration_test.go (2)

134-141: Unsafe type assertions can cause test panics on unexpected responses.

The type assertions at lines 134, 138, 155, and 156 don't use the comma-ok idiom. If the mock server returns an unexpected response structure, these will panic instead of providing a clear test failure.

♻️ Proposed fix
-	result := resp["result"].(map[string]interface{})
-	if result["status"] != "tool_call" {
-		t.Fatalf("expected status 'tool_call', got %v", result["status"])
-	}
-	toolCall := result["tool_call"].(map[string]interface{})
-	if toolCall["name"] != "read_files" {
+	result, ok := resp["result"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected result to be map, got %T", resp["result"])
+	}
+	if result["status"] != "tool_call" {
+		t.Fatalf("expected status 'tool_call', got %v", result["status"])
+	}
+	toolCall, ok := result["tool_call"].(map[string]interface{})
+	if !ok {
+		t.Fatalf("expected tool_call to be map, got %T", result["tool_call"])
+	}
+	if toolCall["name"] != "read_files" {

193-193: Consider handling json.Marshal error in test helper.

While unlikely to fail with the simple map structure, handling this error would make debugging easier if test data becomes malformed.

♻️ Proposed fix
-	body, _ := json.Marshal(req)
+	body, err := json.Marshal(req)
+	if err != nil {
+		t.Fatalf("failed to marshal request: %v", err)
+	}
pkg/mcpclient/mock/server.go (2)

60-64: Use time.Duration constant for better readability.

10 * 1e9 works but is less clear than using the time package constants.

♻️ Proposed fix
+import (
+	// ... existing imports
+	"time"
+)
+
 s.server = &http.Server{
 	Addr:              addr,
 	Handler:           mux,
-	ReadHeaderTimeout: 10 * 1e9, // 10 seconds
+	ReadHeaderTimeout: 10 * time.Second,
 }

88-93: ToolResults returns internal slice reference, allowing external mutation.

The method returns the internal toolResults slice directly. Callers could inadvertently modify it, potentially causing flaky tests. Consider returning a copy.

♻️ Proposed fix
 func (s *Server) ToolResults() []map[string]interface{} {
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	return s.toolResults
+	// Return a copy to prevent external mutation
+	result := make([]map[string]interface{}, len(s.toolResults))
+	copy(result, s.toolResults)
+	return result
 }

Run `make install-hooks` to install a git pre-push hook that
runs `make lint` before each push, preventing lint failures in CI.
- Migrate .golangci.yml to v2 format using golangci-lint migrate
- Remove deprecated linters: tenv, varcheck, typecheck
- Replace tenv with usetesting
- Move goimports to formatters section
- Fix noctx: use exec.CommandContext
- Fix staticcheck ST1005: error strings shouldn't be capitalized or
  end with punctuation - add separate error message constants
- Fix staticcheck QF1008: simplify embedded field selectors
- Fix staticcheck QF1003: use tagged switch instead of if-else
- Fix usetesting: use t.TempDir() and t.Chdir() instead of
  os.MkdirTemp/os.Chdir in tests
- Add make install-hooks target to install pre-push hook
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@pkg/client/entitlement.go`:
- Around line 158-164: The loop constructing EntitlementWithBindings
dereferences v.AppEntitlementView.AppEntitlement without checking for nil;
update the construction in the loop that builds rv to first verify
v.AppEntitlementView != nil and v.AppEntitlementView.AppEntitlement != nil
(similar to GetEntitlement's two-step guard) and skip or handle entries with a
nil AppEntitlementView.AppEntitlement (e.g., continue the loop or set a safe
default) before calling AppEntitlement(*v.AppEntitlementView.AppEntitlement) so
PopulateExpandedMap and bindings remain valid for non-nil entries.

In `@pkg/mcpclient/integration_test.go`:
- Around line 177-213: sendJSONRPC currently ignores json.Marshal errors, can
hang without timeouts, and doesn’t check non-2xx HTTP status; update sendJSONRPC
to capture and handle the error from json.Marshal (fail the test with t.Fatalf),
use a context with timeout or ensure the provided *http.Client has a timeout
(e.g., create ctx, cancel := context.WithTimeout(context.Background(),
time.Second*5) and pass ctx to http.NewRequestWithContext), check
resp.StatusCode and t.Fatalf on non-2xx responses (include status and
body/snippet), and ensure you add the missing import (time) used for the
timeout; keep assertNoRPCError as-is for JSON-RPC payload checks.

In `@pkg/scaffold/scaffold_test.go`:
- Around line 61-65: The test currently only fails when os.Stat returns
IsNotExist, ignoring other errors; update the loop that iterates over
expectedFiles and checks path := filepath.Join(outputDir, f) so that after
calling if _, err := os.Stat(path); you treat any non-nil err as a test failure
(e.g., t.Fatalf or t.Errorf including err) and only skip when err is nil — i.e.,
replace the IsNotExist-only check with a check that fails the test for any err,
referencing the existing os.Stat call, expectedFiles slice, outputDir variable,
and the test's t instance.

Comment on lines 177 to 213
func sendJSONRPC(t *testing.T, client *http.Client, url, method string, params interface{}) map[string]interface{} {
t.Helper()

req := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": method,
}
if params != nil {
req["params"] = params
}

body, _ := json.Marshal(req)
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}

return result
}

func assertNoRPCError(t *testing.T, resp map[string]interface{}) {
t.Helper()
if errObj, ok := resp["error"]; ok && errObj != nil {
t.Fatalf("unexpected JSON-RPC error: %v", errObj)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden sendJSONRPC error handling and timeouts.

json.Marshal errors are ignored, requests can hang indefinitely, and non‑2xx responses aren’t surfaced explicitly. Recommend handling marshal errors, adding a timeout, and checking HTTP status.

✅ Proposed fix
 func sendJSONRPC(t *testing.T, client *http.Client, url, method string, params interface{}) map[string]interface{} {
 	t.Helper()

 	req := map[string]interface{}{
 		"jsonrpc": "2.0",
 		"id":      1,
 		"method":  method,
 	}
 	if params != nil {
 		req["params"] = params
 	}

-	body, _ := json.Marshal(req)
-	httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(body))
+	body, err := json.Marshal(req)
+	if err != nil {
+		t.Fatalf("failed to marshal request: %v", err)
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
 	if err != nil {
 		t.Fatalf("failed to create request: %v", err)
 	}
 	httpReq.Header.Set("Content-Type", "application/json")
 	resp, err := client.Do(httpReq)
 	if err != nil {
 		t.Fatalf("request failed: %v", err)
 	}
 	defer resp.Body.Close()
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		t.Fatalf("unexpected HTTP status: %s", resp.Status)
+	}

 	var result map[string]interface{}
 	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
 		t.Fatalf("failed to decode response: %v", err)
 	}

Also add the missing import:

+	"time"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func sendJSONRPC(t *testing.T, client *http.Client, url, method string, params interface{}) map[string]interface{} {
t.Helper()
req := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": method,
}
if params != nil {
req["params"] = params
}
body, _ := json.Marshal(req)
httpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return result
}
func assertNoRPCError(t *testing.T, resp map[string]interface{}) {
t.Helper()
if errObj, ok := resp["error"]; ok && errObj != nil {
t.Fatalf("unexpected JSON-RPC error: %v", errObj)
}
func sendJSONRPC(t *testing.T, client *http.Client, url, method string, params interface{}) map[string]interface{} {
t.Helper()
req := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": method,
}
if params != nil {
req["params"] = params
}
body, err := json.Marshal(req)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
t.Fatalf("unexpected HTTP status: %s", resp.Status)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return result
}
func assertNoRPCError(t *testing.T, resp map[string]interface{}) {
t.Helper()
if errObj, ok := resp["error"]; ok && errObj != nil {
t.Fatalf("unexpected JSON-RPC error: %v", errObj)
}
}
🤖 Prompt for AI Agents
In `@pkg/mcpclient/integration_test.go` around lines 177 - 213, sendJSONRPC
currently ignores json.Marshal errors, can hang without timeouts, and doesn’t
check non-2xx HTTP status; update sendJSONRPC to capture and handle the error
from json.Marshal (fail the test with t.Fatalf), use a context with timeout or
ensure the provided *http.Client has a timeout (e.g., create ctx, cancel :=
context.WithTimeout(context.Background(), time.Second*5) and pass ctx to
http.NewRequestWithContext), check resp.StatusCode and t.Fatalf on non-2xx
responses (include status and body/snippet), and ensure you add the missing
import (time) used for the timeout; keep assertNoRPCError as-is for JSON-RPC
payload checks.

Comment on lines +61 to +65
for _, f := range expectedFiles {
path := filepath.Join(outputDir, f)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected file %s to exist", f)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle non-IsNotExist errors from os.Stat.

Right now any stat error other than “not exists” is silently ignored, which can hide failures (e.g., permission or transient IO). Treat any error as a test failure.

💡 Suggested fix
 	for _, f := range expectedFiles {
 		path := filepath.Join(outputDir, f)
-		if _, err := os.Stat(path); os.IsNotExist(err) {
-			t.Errorf("expected file %s to exist", f)
-		}
+		if _, err := os.Stat(path); err != nil {
+			if os.IsNotExist(err) {
+				t.Errorf("expected file %s to exist", f)
+			} else {
+				t.Fatalf("failed to stat %s: %v", f, err)
+			}
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, f := range expectedFiles {
path := filepath.Join(outputDir, f)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("expected file %s to exist", f)
}
for _, f := range expectedFiles {
path := filepath.Join(outputDir, f)
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
t.Errorf("expected file %s to exist", f)
} else {
t.Fatalf("failed to stat %s: %v", f, err)
}
}
}
🤖 Prompt for AI Agents
In `@pkg/scaffold/scaffold_test.go` around lines 61 - 65, The test currently only
fails when os.Stat returns IsNotExist, ignoring other errors; update the loop
that iterates over expectedFiles and checks path := filepath.Join(outputDir, f)
so that after calling if _, err := os.Stat(path); you treat any non-nil err as a
test failure (e.g., t.Fatalf or t.Errorf including err) and only skip when err
is nil — i.e., replace the IsNotExist-only check with a check that fails the
test for any err, referencing the existing os.Stat call, expectedFiles slice,
outputDir variable, and the test's t instance.

CodeRabbit feedback:
- pkg/client/entitlement.go: add nil check before dereferencing
  AppEntitlementView.AppEntitlement in loop
- pkg/mcpclient/client.go: guard against nil ToolHandler before
  calling HandleToolCall
- pkg/mcpclient/mock/server.go: log encoding errors in sendResult
  and sendError instead of ignoring them
- pkg/mcpclient/mock/server_test.go: handle json.Marshal errors
  with t.Fatalf instead of ignoring
- pkg/prompt/prompt.go: prevent DisplayBox panic when title or
  content line exceeds box width

Scaffold template improvements based on learnings:
- Add Entity Confusion warning to Grant/Revoke - documents which
  entity provides which data (principal=who, entitlement=what)
- Add idempotency handling examples - show proper handling of
  "already exists" and "not found" errors
- Add pagination termination comment - use API's next token,
  not result count
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pkg/mcpclient/client.go`:
- Around line 26-27: The doc comment for HTTPClient promises a nil-safe default
but sendRequest dereferences HTTPClient directly; either ensure a nil fallback
is used or update the comment — to fix, add a nil-check in sendRequest that uses
http.DefaultClient (or c.NewClient-created default) when c.HTTPClient == nil,
e.g., reference the HTTPClient field and the sendRequest method to locate where
to insert the fallback, and keep NewClient as-is (or alternatively update the
HTTPClient comment to state it must be non-nil if you choose not to add the
fallback).

In `@pkg/scaffold/scaffold.go`:
- Around line 79-88: The current file creation in the scaffold logic uses
os.OpenFile with os.O_TRUNC which silently overwrites existing files; change the
open flags in the scaffold creation code (the os.OpenFile call that uses
fullPath and mode derived from tf.Mode) to use os.O_CREATE|os.O_EXCL|os.O_WRONLY
so the call fails if the target file already exists, then detect that error
(os.IsExist) and return a clear error message indicating the file already exists
(so callers can implement a --force behavior later); keep the existing default
mode fallback (mode == 0 -> 0644) and ensure the code still defers f.Close()
only on successful open.
🧹 Nitpick comments (5)
pkg/client/entitlement.go (1)

68-87: Consider adding defensive nil check for consistency.

The GetPaths method dereferences e.AppEntitlementView at line 72 without a nil check. While the NewExpandableEntitlementWithBindings constructor guarantees non-nil AppEntitlementView for valid instances, adding a defensive check would be consistent with the guard added at lines 161-163 in SearchEntitlements.

♻️ Optional defensive check
 func (e *ExpandableEntitlementWithBindings) GetPaths() []PathDetails {
 	if e == nil {
 		return nil
 	}
+	if e.AppEntitlementView == nil {
+		return nil
+	}
 	view := *e.AppEntitlementView
pkg/mcpclient/mock/server.go (2)

61-74: Use time.Duration constant instead of numeric multiplication.

10 * 1e9 is less readable than 10 * time.Second and requires the reader to know that nanoseconds are expected.

Also, Start() returns before the server is confirmed listening, which can cause race conditions in tests if callers immediately try to connect.

♻️ Suggested improvement
 	s.server = &http.Server{
 		Addr:              addr,
 		Handler:           mux,
-		ReadHeaderTimeout: 10 * 1e9, // 10 seconds
+		ReadHeaderTimeout: 10 * time.Second,
 	}

89-94: Consider returning a copy of the slice for thread safety.

The current implementation returns the internal slice reference after releasing the lock. Callers could inadvertently modify it, or observe modifications made by concurrent handleToolResult calls.

For a test mock this is likely acceptable, but a defensive copy would be safer.

♻️ Defensive copy
 func (s *Server) ToolResults() []map[string]interface{} {
 	s.mu.Lock()
 	defer s.mu.Unlock()
-	return s.toolResults
+	result := make([]map[string]interface{}, len(s.toolResults))
+	copy(result, s.toolResults)
+	return result
 }
pkg/prompt/prompt.go (2)

17-24: Consider using or removing ErrCancelled.

ErrCancelled is defined but never returned by any function in this package. If signal handling (Ctrl+C) is planned, this is fine to keep. Otherwise, consider removing dead code or adding a TODO comment indicating the intended future use.


247-268: Optional: Consider deduplicating selected indices.

If the user enters 1,2,1, the function returns [0, 1, 0] with duplicates. This may be intentional, but typically multi-select returns unique indices. A simple map-based dedup could prevent unexpected behavior.

Comment on lines +79 to +88
// Create file
mode := tf.Mode
if mode == 0 {
mode = 0644
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent silent overwrites of existing files.
os.O_TRUNC will overwrite any existing files if the user runs scaffold in a non-empty directory, risking data loss. Consider failing when a target file already exists (and optionally add a --force flag later).

💡 Safer default (fail if file exists)
 import (
+	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
 	"strings"
 	"text/template"
 )
@@
-	f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
+	f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, mode)
 	if err != nil {
+		if errors.Is(err, os.ErrExist) {
+			return fmt.Errorf("file already exists: %s", fullPath)
+		}
 		return fmt.Errorf("failed to create file: %w", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create file
mode := tf.Mode
if mode == 0 {
mode = 0644
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
)
Suggested change
// Create file
mode := tf.Mode
if mode == 0 {
mode = 0644
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
// Create file
mode := tf.Mode
if mode == 0 {
mode = 0644
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, mode)
if err != nil {
if errors.Is(err, os.ErrExist) {
return fmt.Errorf("file already exists: %s", fullPath)
}
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
🤖 Prompt for AI Agents
In `@pkg/scaffold/scaffold.go` around lines 79 - 88, The current file creation in
the scaffold logic uses os.OpenFile with os.O_TRUNC which silently overwrites
existing files; change the open flags in the scaffold creation code (the
os.OpenFile call that uses fullPath and mode derived from tf.Mode) to use
os.O_CREATE|os.O_EXCL|os.O_WRONLY so the call fails if the target file already
exists, then detect that error (os.IsExist) and return a clear error message
indicating the file already exists (so callers can implement a --force behavior
later); keep the existing default mode fallback (mode == 0 -> 0644) and ensure
the code still defers f.Close() only on successful open.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/mcpclient/mock/server.go`:
- Around line 90-95: ToolResults currently returns the internal slice
s.toolResults while holding the lock, which allows handleToolResult to mutate
the slice after the lock is released and causes a data race; fix by, inside
Server.ToolResults (while mu is locked), allocate a new slice of the same
length, copy the elements from s.toolResults into it (e.g.
make([]map[string]interface{}, len(s.toolResults)); copy(...)), then return that
new slice so callers get a snapshot and the internal slice can't be mutated
concurrently; keep the mu.Lock()/defer mu.Unlock() around the copy operation and
reference Server.ToolResults, s.toolResults, and handleToolResult to locate the
code.
🧹 Nitpick comments (1)
pkg/mcpclient/mock/server.go (1)

56-75: Server may not be listening when Start returns.

Start() returns immediately while ListenAndServe runs in a goroutine. Tests that connect right after Start() may experience intermittent failures if the socket isn't bound yet. Additionally, startup errors (e.g., port already in use) are silently swallowed.

Consider waiting for the server to be ready before returning, or at minimum providing a mechanism for callers to wait.

♻️ Suggested fix using a channel to signal readiness
 func (s *Server) Start(addr string) error {
 	s.addr = addr
 	mux := http.NewServeMux()
 	mux.HandleFunc("/", s.HandleMCP)

-	s.server = &http.Server{
-		Addr:              addr,
-		Handler:           mux,
-		ReadHeaderTimeout: 10 * time.Second,
-	}
+	ln, err := net.Listen("tcp", addr)
+	if err != nil {
+		return fmt.Errorf("failed to listen on %s: %w", addr, err)
+	}
+	s.addr = ln.Addr().String() // capture actual address (useful for ":0")
+
+	s.server = &http.Server{
+		Handler:           mux,
+		ReadHeaderTimeout: 10 * time.Second,
+	}

 	go func() {
-		if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
+		if err := s.server.Serve(ln); err != http.ErrServerClosed {
 			fmt.Printf("mock server error: %v\n", err)
 		}
 	}()

 	return nil
 }

This also requires adding "net" to the imports.

@robert-chiniquy robert-chiniquy changed the title DRAFT: Rch/scaffold and publish RFR: Rch/scaffold and publish Jan 27, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@pkg/mcpclient/mock/server.go`:
- Around line 56-74: The Start method currently sets s.addr to the requested
addr and starts ListenAndServe in a goroutine, which hides bind failures and
leaves s.addr wrong for ephemeral ports; change Server.Start (s.addr, s.server)
to explicitly bind using net.Listen (e.g., net.Listen("tcp", addr)), return any
listen error immediately, create http.Server and call Serve(listener) in the
goroutine, and set s.addr to listener.Addr().String() after successful listen so
callers can use Addr() to connect; also propagate any listen error instead of
always returning nil.
- Around line 172-186: handleToolsCall resets s.currentStep but leaves
s.toolResults intact and doesn't guard against a nil scenario, causing results
to accumulate across runs and a possible panic in sendNextToolCallback; update
handleToolsCall to also clear s.toolResults (e.g., reset or reinitialize the
slice/map) while holding s.mu, and check that s.scenario is not nil before
calling sendNextToolCallback (returning a proper error via s.sendError if nil)
so each run starts fresh and avoids nil derefs in sendNextToolCallback.

Comment on lines 56 to 74
// Start starts the mock server on the given address.
func (s *Server) Start(addr string) error {
s.addr = addr
mux := http.NewServeMux()
mux.HandleFunc("/", s.HandleMCP)

s.server = &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}

go func() {
if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("mock server error: %v\n", err)
}
}()

return nil
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expose actual bound address and propagate bind failures.

Line 58 sets s.addr to the requested address verbatim, so passing ":0" leaves Addr() unusable for clients. Also, Line 69 logs ListenAndServe failures but Start still returns nil. Consider binding explicitly and storing the real address.

🐛 Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net"
 	"net/http"
 	"os"
 	"sync"
 	"time"
 )
 
 func (s *Server) Start(addr string) error {
-	s.addr = addr
 	mux := http.NewServeMux()
 	mux.HandleFunc("/", s.HandleMCP)
 
+	ln, err := net.Listen("tcp", addr)
+	if err != nil {
+		return err
+	}
+	s.addr = ln.Addr().String()
+
 	s.server = &http.Server{
 		Addr:              addr,
 		Handler:           mux,
 		ReadHeaderTimeout: 10 * time.Second,
 	}
 
 	go func() {
-		if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
+		if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
 			fmt.Printf("mock server error: %v\n", err)
 		}
 	}()
 
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Start starts the mock server on the given address.
func (s *Server) Start(addr string) error {
s.addr = addr
mux := http.NewServeMux()
mux.HandleFunc("/", s.HandleMCP)
s.server = &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
fmt.Printf("mock server error: %v\n", err)
}
}()
return nil
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"sync"
"time"
)
// Start starts the mock server on the given address.
func (s *Server) Start(addr string) error {
mux := http.NewServeMux()
mux.HandleFunc("/", s.HandleMCP)
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
s.addr = ln.Addr().String()
s.server = &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed {
fmt.Printf("mock server error: %v\n", err)
}
}()
return nil
}
🤖 Prompt for AI Agents
In `@pkg/mcpclient/mock/server.go` around lines 56 - 74, The Start method
currently sets s.addr to the requested addr and starts ListenAndServe in a
goroutine, which hides bind failures and leaves s.addr wrong for ephemeral
ports; change Server.Start (s.addr, s.server) to explicitly bind using
net.Listen (e.g., net.Listen("tcp", addr)), return any listen error immediately,
create http.Server and call Serve(listener) in the goroutine, and set s.addr to
listener.Addr().String() after successful listen so callers can use Addr() to
connect; also propagate any listen error instead of always returning nil.

Comment on lines 172 to 186
func (s *Server) handleToolsCall(w http.ResponseWriter, id interface{}, params map[string]interface{}) {
name, _ := params["name"].(string)

if name != "connector_analyze" {
s.sendError(w, id, -32602, fmt.Sprintf("unknown tool: %s", name))
return
}

s.mu.Lock()
s.currentStep = 0
s.mu.Unlock()

// Return the first tool callback
s.sendNextToolCallback(w, id)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset per-run state and guard missing scenario.

Line 180 resets currentStep but leaves toolResults intact, so results accumulate across multiple runs and the summary becomes misleading. Also, a nil scenario will panic in sendNextToolCallback.

🐛 Proposed fix
 func (s *Server) handleToolsCall(w http.ResponseWriter, id interface{}, params map[string]interface{}) {
 	name, _ := params["name"].(string)
 
 	if name != "connector_analyze" {
 		s.sendError(w, id, -32602, fmt.Sprintf("unknown tool: %s", name))
 		return
 	}
+	if s.scenario == nil {
+		s.sendError(w, id, -32603, "scenario not configured")
+		return
+	}
 
 	s.mu.Lock()
 	s.currentStep = 0
+	s.toolResults = s.toolResults[:0]
 	s.mu.Unlock()
 
 	// Return the first tool callback
 	s.sendNextToolCallback(w, id)
 }
🤖 Prompt for AI Agents
In `@pkg/mcpclient/mock/server.go` around lines 172 - 186, handleToolsCall resets
s.currentStep but leaves s.toolResults intact and doesn't guard against a nil
scenario, causing results to accumulate across runs and a possible panic in
sendNextToolCallback; update handleToolsCall to also clear s.toolResults (e.g.,
reset or reinitialize the slice/map) while holding s.mu, and check that
s.scenario is not nil before calling sendNextToolCallback (returning a proper
error via s.sendError if nil) so each run starts fresh and avoids nil derefs in
sendNextToolCallback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants