Skip to content

feat: add gopls LSP plugin support for Claude code running in the sandbox#815

Merged
rh-hemartin merged 2 commits into
fullsend-ai:mainfrom
gklein:feat/gopls-lsp-plugin
May 14, 2026
Merged

feat: add gopls LSP plugin support for Claude code running in the sandbox#815
rh-hemartin merged 2 commits into
fullsend-ai:mainfrom
gklein:feat/gopls-lsp-plugin

Conversation

@gklein
Copy link
Copy Markdown
Contributor

@gklein gklein commented May 11, 2026

Summary

Adds gopls (Go Language Server) support to the code agent sandbox, enabling Claude Code's LSP tool for semantic Go code intelligence inside OpenShell sandboxes.

Related to #678 and #718

Problem

The code agent relies entirely on grep/glob/Read for code navigation. Without a language server, it misses type-aware resolution — interface implementations, cross-package references, embedded struct fields, and call hierarchy. Claude Code's LSP tool exists but requires two things the sandbox lacked:

  1. A language server binary (gopls) installed and on PATH
  2. A marketplace plugin that configures the LSP server — Claude Code only registers the LSP tool when the lspServers config comes from a marketplace plugin definition, not from inline --plugin-dir plugins

Changes

Container image (images/code/Containerfile, +6 lines)

  • Installs gopls v0.18.1 via go install at /usr/local/go/bin/gopls

Harness schema (internal/harness/harness.go, +20 lines)

  • Adds generic Plugins []string field (mirrors Skills pattern)
  • Plugin name validation via validPluginName regex (^[a-zA-Z0-9_-]+$)
  • Path resolution with traversal protection, file existence validation

Runner (internal/cli/run.go, +140 lines)

  • bootstrapPlugins function creates the Claude Code marketplace plugin file structure inside the sandbox:
    • marketplace.json (with required owner field and lspServers config read from .lsp.json)
    • known_marketplaces.json, installed_plugins.json, settings.json with enabledPlugins
    • Cache and source directories with README stubs
    • All mkdir+echo batched into a single sandbox.Exec call
  • buildClaudeCommand passes --plugin-dir flags for defense-in-depth
  • Adds /usr/local/go/bin to sandbox PATH (Docker ENV is overridden by sandbox .bashrc)
  • Displays configured plugins in the run preamble

Scaffold (internal/scaffold/fullsend-repo/, +5 lines)

  • plugins/gopls-lsp/plugin.json + .lsp.json — gopls LSP server config
  • harness/code.yaml — adds plugins: [plugins/gopls-lsp]

Tests (internal/cli/run_test.go, +9 lines)

  • Updates existing buildClaudeCommand tests for new signature
  • Adds TestBuildClaudeCommand_WithPluginDirs

Why marketplace emulation?

Claude Code's LSP tool is only registered when lspServers config comes from a marketplace plugin definition (marketplace.json), not from inline --plugin-dir plugins. We verified this empirically:

  • --plugin-dir → plugin loads, LSP tool NOT registered (23 tools)
  • Marketplace cache → plugin loads, LSP tool registered (31 tools, 17 LSP calls in test run)

In --print mode (which fullsend uses), Claude Code skips marketplace sync, so we pre-populate the marketplace file structure during sandbox bootstrap. The structure matches anthropics/claude-plugins-official.

Test results

End-to-end fullsend workflow inside OpenShell sandbox with a Go test project containing a multi-level call chain (main → orchestrate → buildGreeting → getPrefix → formatOutput):

LSP Operation Count Result
documentSymbol 4 Found all 5 functions with types and line numbers
outgoingCalls 6 orchestrate → buildGreeting + formatOutput
incomingCalls 3 buildGreeting ← orchestrate
goToDefinition 2 getPrefix defined at line 20:6
prepareCallHierarchy 1 Call hierarchy preparation
hover 1 Type information
Total LSP calls 17 All successful

Security

  • Plugin names validated against ^[a-zA-Z0-9_-]+$ before shell interpolation (matches existing validAgentName pattern)
  • Path traversal prevention via ResolveRelativeTo with directory containment check
  • .lsp.json content parsed via json.Unmarshal and re-serialized via json.Marshal (no raw interpolation)
  • No new network endpoints — gopls works offline for local code intelligence
  • Reviewed via /security-review: no findings above confidence threshold

Test plan

  • go test ./internal/cli/ ./internal/harness/ — all tests pass
  • go vet ./... — clean
  • Container image builds with gopls (gopls version succeeds)
  • fullsend workflow runs in OpenShell sandbox with LSP tool calls
  • LSP documentSymbol, outgoingCalls, incomingCalls, goToDefinition all return correct results
  • gopls works without **/gopls in network policy (no network needed for local projects)
  • Security review: no vulnerabilities found

Install gopls v0.18.1 in the code agent container image and add a
generic Plugins field to the harness schema. The runner bootstraps
plugins as Claude Code marketplace-cached plugins so the LSP tool
registers for semantic Go code intelligence (go-to-definition,
find-references, call hierarchy) inside OpenShell sandboxes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

fullsend review is working on this — view logs

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

Site preview

Preview: https://62154cf1-site.fullsend-ai.workers.dev

Commit: 1ffc74acf41f9f23646643d13146c6602e79ff9f

@fullsend-ai-review
Copy link
Copy Markdown

fullsend-ai-review Bot commented May 11, 2026

Review: #815

Head SHA: 1ffc74a
Timestamp: 2026-05-14T00:00:00Z
Outcome: approve

Summary

This PR adds gopls (Go Language Server) support to the code agent sandbox, enabling Claude Code's LSP tool for semantic Go code intelligence. The implementation is well-structured: it adds a generic Plugins field to the harness schema, validates plugin names against ^[a-zA-Z0-9_-]+$, resolves paths with traversal protection, scans plugin files for injection before copying, and replicates the Claude Code marketplace file structure to trigger LSP tool registration. The code follows existing patterns in the codebase (mirroring the Skills implementation), has thorough test coverage for the new buildPluginConfigs function and harness validation, and correctly handles edge cases (missing .lsp.json, invalid JSON, empty plugin list). No critical or high findings; two medium observations are noted below but neither blocks the change.

Findings

Medium

  • [Style/conventions] internal/cli/run.go:811 — Go-specific PATH addition in generic plugin code path. The line pathExport += ":/usr/local/go/bin" is added whenever any plugins are configured, not only when gopls is used. If a non-Go plugin (e.g., a Python or TypeScript LSP) is added later, the Go binary path is still unconditionally injected. Consider making PATH additions plugin-specific (e.g., via a field in plugin.json or .lsp.json) or documenting this as a known limitation to address when a second language server is added.

Low

  • [Style/conventions] internal/cli/run.go:1503 — Hardcoded timestamp "2026-01-01T00:00:00.000Z" in marketplace config entries. While functional (Claude Code likely doesn't validate these dates), a static future date may confuse debugging. Consider using time.Now().UTC().Format(time.RFC3339) or a comment explaining why a fixed value was chosen (e.g., deterministic sandbox bootstrapping).

Info

  • [Correctness] The bootstrapPlugins function itself has no unit test, but this is reasonable since it requires a running sandbox. The pure logic (buildPluginConfigs) is thoroughly tested with 7 test cases covering single/multiple plugins, missing/invalid/empty .lsp.json, config structure validation, and empty plugin lists.
  • [Intent alignment] The PR aligns with both referenced issues (Add language servers to the code sandbox image and enable ENABLE_LSP_TOOL #678: add language servers to sandbox, Claude Code plugins are not available in fullsend sandbox containers #718: plugins not available in sandbox). The scope is appropriate — it adds the generic plugin infrastructure and one concrete plugin (gopls).
  • [Platform security] Plugin name validation, path traversal protection via ResolveRelativeTo, .lsp.json content parsed via json.Unmarshal/json.Marshal (no raw shell interpolation), and injection scanning of plugin files before sandbox copy are all properly implemented.
  • [Injection defense] PR body and commit messages contain no prompt injection patterns or non-rendering Unicode. plugin.json and .lsp.json are added to the ScannableFiles map for injection scanning.
  • [Content security] No new user-facing content handling. Plugin content is developer-authored and scanned before use.

Footer

Outcome: approve
This review applies to SHA 1ffc74acf41f9f23646643d13146c6602e79ff9f. Any push to the PR head clears this review and requires a new evaluation.

Previous run

Review: #815

Head SHA: fdcf618
Timestamp: 2026-05-13T00:00:00Z
Outcome: request-changes

Summary

The PR adds gopls LSP plugin support to the sandbox — a well-structured feature with good security hygiene (input validation, injection scanning, path traversal protection, JSON re-serialization). The marketplace emulation approach is clearly motivated by an empirical limitation in Claude Code's --print mode. However, the PR commits a .DS_Store binary file to the repository root, which must be removed before merge. There are also several medium-severity correctness and extensibility concerns.

Findings

High

  • [Style/conventions] .DS_Store — macOS Finder metadata binary file committed to repository root. This file exposes directory metadata, creates merge conflicts for other macOS users, and is never appropriate in version control. It must be removed from this PR and .DS_Store should be added to .gitignore.
    Remediation: git rm --cached .DS_Store and add .DS_Store to .gitignore.

Medium

  • [Correctness] internal/cli/run.go (bootstrapEnv PATH modification) — The PATH extension /usr/local/go/bin is hardcoded and gated on len(h.Plugins) > 0 rather than being tied to a specific plugin's requirements. If a non-Go plugin is added (e.g., typescript-language-server per Add language servers to the code sandbox image and enable ENABLE_LSP_TOOL #678), it would get the Go path but not its own. Consider making PATH additions a per-plugin configuration field or deriving them from plugin metadata.
    Remediation: Add an optional path or bin_dirs field to plugin configuration, or document that PATH additions must be updated when new language server plugins are added.

  • [Correctness] internal/cli/run.go (buildPluginConfigs) — When .lsp.json exists but contains invalid JSON, the error is silently swallowed (if json.Unmarshal(data, &servers) == nil). The plugin will be installed without LSP server configuration and no diagnostic is emitted, making this failure mode difficult to debug.
    Remediation: Log a warning (to stderr, consistent with existing patterns) when .lsp.json exists but fails to parse.

  • [Correctness] internal/cli/run.go (buildPluginConfigs) — The function produces 4 config entries even when called with an empty/nil plugin list (as verified by TestBuildPluginConfigs_EmptyPluginList). While the caller guards with len(h.Plugins) > 0, the function itself does not validate its input, creating a latent bug if a future caller omits the guard.
    Remediation: Return early with nil entries when the plugins slice is empty.

Low

  • [Style/conventions] internal/cli/run.go — The debug log extraction feature (FULLSEND_CLAUDE_DEBUG env var, --debug-file flag, debug log download) is bundled with the plugin feature but is an independent concern. Consider splitting it into a separate commit or PR for clearer change history.

  • [Correctness] internal/scaffold/fullsend-repo/plugins/gopls-lsp/.lsp.json — The extensionToLanguage mapping only includes .go. Go projects may contain .go.tmpl, .go2, or test fixture files with non-standard extensions. While gopls handles these gracefully, the mapping is narrower than typical gopls configurations.

Info

Footer

Outcome: request-changes
This review applies to SHA fdcf618ab886a2e138309b1f0138335ab54d6a44. Any push to the PR head clears this review and requires a new evaluation.

Previous run (2)

Review: #815

Head SHA: ba52a68
Timestamp: 2026-05-11T00:00:00Z
Outcome: comment-only

Summary

This PR adds gopls language server support to the code agent sandbox by installing the binary in the container image, adding a Plugins field to the harness schema, and bootstrapping a marketplace-emulation file structure so Claude Code registers the LSP tool. The approach is well-reasoned and the marketplace emulation is clearly documented. No critical or high findings; four medium/low items are worth addressing in follow-up.

Findings

Medium

  • [Correctness] internal/cli/run.go:~1497 (bootstrapPlugins, config file upload loop) — tmp.Write(data) does not check the returned error. If the write fails (e.g., disk full), a partial or empty temp file is silently uploaded to the sandbox, producing a corrupt config. Compare with bootstrapSecurityHooks (line 1347) which correctly checks the write error.
    Remediation: Check the error from tmp.Write(data) and return early on failure, matching the pattern used elsewhere in this file.

  • [Correctness] internal/cli/run.go:787 — The PATH modification (/usr/local/go/bin) is unconditional for all agents, not just those with gopls configured. While harmless (an extra PATH entry for a directory that exists in the code image), it couples every agent's bootstrap to a Go-specific path. If other images don't have /usr/local/go this adds a dead PATH entry.
    Remediation: Consider gating the /usr/local/go/bin PATH addition on len(h.Plugins) > 0 or on a more general mechanism, or accept the coupling as intentional since the code image always has Go installed.

  • [Platform Security] internal/cli/run.go:~1460-1475 (bootstrapPlugins) — Plugin directories are uploaded to the sandbox without passing through the security scanPipeline. Skills receive injection scanning before upload (lines 706-740 in bootstrapSandbox), but plugins skip this step entirely. While plugin content is currently JSON config with low injection surface, this breaks the defense-in-depth pattern established for skills.
    Remediation: Add plugin content scanning consistent with the skill scanning pattern, or document the intentional omission with a code comment explaining why plugins are exempt.

Low

  • [Correctness] internal/cli/run_test.gobootstrapPlugins is ~80 lines handling filesystem operations, JSON marshaling, sandbox uploads, and marketplace structure creation, but has no unit test. The only new test (TestBuildClaudeCommand_WithPluginDirs) covers simple flag concatenation. Edge cases like malformed .lsp.json, missing plugin directories, or upload failures are untested.
    Remediation: Add unit tests for bootstrapPlugins covering at least the happy path and error paths (missing .lsp.json, invalid JSON).

Info

Footer

Outcome: comment-only
This review applies to SHA ba52a683a2516f63228ff02a75393d67d38f786d. Any push to the PR head clears this review and requires a new evaluation.

@rh-hemartin
Copy link
Copy Markdown
Contributor

Hello! Could you send us the workflows in which this was tested? I'm interested in the transcripts to make sure it is recognizing the feature and using it properly.

@gklein
Copy link
Copy Markdown
Contributor Author

gklein commented May 12, 2026

Hello! Could you send us the workflows in which this was tested? I'm interested in the transcripts to make sure it is >recognizing the feature and using it properly.

gopls LSP Plugin — End-to-End Validation

Experiment configuration files

experiments/gopls-lsp-validation/.fullsend/harness/lsp-callgraph.yaml

agent: agents/lsp-callgraph.md
model: sonnet
image: fullsend-code:lsp-test
policy: policies/lsp-callgraph.yaml

plugins:
  - plugins/gopls-lsp

host_files:
  - src: env/gcp-vertex.env
    dest: /tmp/workspace/.env.d/gcp-vertex.env
    expand: true
  - src: ${GOOGLE_APPLICATION_CREDENTIALS}
    dest: /tmp/workspace/.gcp-credentials.json

timeout_minutes: 10

security:
  enabled: false

experiments/gopls-lsp-validation/.fullsend/agents/lsp-callgraph.md

---
name: lsp-callgraph
description: Analyze Go code call graph using the LSP tool
---

# LSP Call Graph Analyzer

You MUST use the **LSP tool** as your primary and first tool for ALL code analysis. Do NOT read files or grep before using the LSP tool.

## Task

1. Use LSP `documentSymbol` on main.go (line 1, character 1) to list all functions.
2. Use LSP `outgoingCalls` on the `orchestrate` function (line 10, character 6).
3. Use LSP `incomingCalls` on the `buildGreeting` function (line 15, character 6).
4. Use LSP `goToDefinition` on the `getPrefix()` call inside buildGreeting (line 16, character 12).
5. Write results to `$FULLSEND_OUTPUT_DIR/lsp-analysis.md` with the full call chain from `main()` to `getPrefix()`.

experiments/gopls-lsp-validation/.fullsend/plugins/gopls-lsp/plugin.json

{"name": "gopls-lsp"}

experiments/gopls-lsp-validation/.fullsend/plugins/gopls-lsp/.lsp.json

{"go":{"command":"gopls","args":["serve"],"extensionToLanguage":{".go":"go"}}}

experiments/gopls-lsp-validation/.fullsend/policies/lsp-callgraph.yaml

version: 1
filesystem_policy:
  include_workdir: true
  read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log]
  read_write: [/sandbox, /tmp, /dev/null]
landlock:
  compatibility: best_effort
process:
  run_as_user: sandbox
  run_as_group: sandbox
network_policies:
  vertex_ai:
    name: vertex-ai
    endpoints:
      - host: "*.googleapis.com"
        port: 443
        protocol: rest
        enforcement: enforce
        access: full
    binaries:
      - path: "**/claude"
      - path: "**/node"
  go_modules:
    name: go-modules
    endpoints:
      - host: "proxy.golang.org"
        port: 443
        protocol: rest
        enforcement: enforce
        access: full
      - host: "sum.golang.org"
        port: 443
        protocol: rest
        enforcement: enforce
        access: full
      - host: "storage.googleapis.com"
        port: 443
        protocol: rest
        enforcement: enforce
        access: full
    binaries:
      - path: "**/go"

experiments/gopls-lsp-validation/.fullsend/env/gcp-vertex.env

export CLAUDE_CODE_USE_VERTEX=1
export ANTHROPIC_VERTEX_PROJECT_ID="${ANTHROPIC_VERTEX_PROJECT_ID}"
export CLOUD_ML_REGION="${CLOUD_ML_REGION}"
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/workspace/.gcp-credentials.json

experiments/gopls-lsp-validation/.fullsend/target-repo/main.go

package main

import "fmt"

func main() {
	result := orchestrate("world")
	fmt.Println(result)
}

func orchestrate(name string) string {
	greeting := buildGreeting(name)
	return formatOutput(greeting)
}

func buildGreeting(name string) string {
	prefix := getPrefix()
	return prefix + ", " + name
}

func getPrefix() string {
	return "Hello"
}

func formatOutput(s string) string {
	return "[output] " + s
}

experiments/gopls-lsp-validation/.fullsend/target-repo/go.mod

module lsp-callgraph-test

go 1.24

Fullsend Run

$ /bin/fullsend run lsp-callgraph --fullsend-dir experiments/gopls-lsp-validation/.fullsend --target-repo experiments/gopls-lsp-validation/.fullsend/target-repo --output-dir /tmp/fullsend-lsp-test --no-post-script

⚡ fullsend
  Autonomous agentic development for GitHub organizations

→ Running agent: lsp-callgraph

  • Loading harness: experiments/gopls-lsp-validation/.fullsend/harness/lsp-callgraph.yaml
  ✓ Harness loaded (0.0s)
    Agent: fullsend/experiments/gopls-lsp-validation/.fullsend/agents/lsp-callgraph.md
    Policy: fullsend/experiments/gopls-lsp-validation/.fullsend/policies/lsp-callgraph.yaml
    Model: sonnet
    Image: fullsend-code:lsp-test
    Plugins: fullsend/experiments/gopls-lsp-validation/.fullsend/plugins/gopls-lsp
    Timeout: 10 minutes

  • Checking openshell availability
  ✓ openshell available (0.0s)
  • Ensuring gateway
  ✓ Gateway ready (0.0s)
  • Creating sandbox: agent-lsp-callgraph-42943-1778595166
  ✓ Sandbox created (0.4s)
  • Bootstrapping sandbox
Cross-compiling fullsend for linux/arm64...
Cross-compiled fullsend for linux/arm64
  ✓ Sandbox bootstrapped (1.8s)
  • Copying project code into sandbox
  ✓ Project code copied to target-repo/ (0.1s)
    Trace ID: 6b0d0e09-59ab-42da-888e-03826dec9daf
  • Running agent

  ⟳ Agent running (30s elapsed, 9m30s remaining)
  ⟳ Agent running (1m0s elapsed, 9m0s remaining)

  ✓ Agent exited with code 0 (78.8s)
  • Extracting output files
    /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/output/lsp-analysis.md
  ✓ Extracted 1 output file(s) (0.1s)
  • Extracting transcripts
  [lsp-callgraph] Saved transcript: lsp-callgraph-ef046633-6e86-482a-bf53-7c8cf868e0a8.jsonl
  [lsp-callgraph] Saved transcript: lsp-callgraph-agent-a53a1c01ff6cdc8a8.jsonl
  ✓ Transcripts extracted (0.1s)
    Extracted claude-debug.log
  • Extracting target repo
  ✓ Target repo extracted to fullsend/experiments/gopls-lsp-validation/.fullsend/target-repo (0.1s)

→ Results
    Run directory: /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166
    Agent exit code: 0
    Agent runs: 1
    Trace ID: 6b0d0e09-59ab-42da-888e-03826dec9daf

  • Collecting OpenShell logs
  ✓ Collected 2 OpenShell log source(s) to /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/logs
  • Cleaning up sandbox
  ✓ Sandbox deleted (0.1s)

$ RUN=/tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166

Debug log — gopls server lifecycle

$ grep -E LSP MANAGER|LSP server|Loaded.*LSP|LSP PROTOCOL|LSP client /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/claude-debug.log

2026-05-12T14:12:49.230Z [DEBUG] [LSP MANAGER] initializeLspServerManager() called
2026-05-12T14:12:49.230Z [DEBUG] [LSP MANAGER] Created manager instance, state=pending
2026-05-12T14:12:49.230Z [DEBUG] [LSP MANAGER] Starting async initialization (generation 1)
2026-05-12T14:12:49.248Z [DEBUG] Loaded 1 LSP server(s) from plugin: gopls-lsp
2026-05-12T14:12:49.248Z [DEBUG] Total LSP servers loaded: 1
2026-05-12T14:12:49.249Z [DEBUG] LSP server manager initialized successfully
2026-05-12T14:13:20.239Z [DEBUG] Starting LSP server instance: plugin:gopls-lsp:go
2026-05-12T14:13:20.247Z [DEBUG] LSP client started for plugin:gopls-lsp:go
2026-05-12T14:13:20.247Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'initialize - (0)'.
2026-05-12T14:13:20.313Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'initialize - (0)' in 66ms.
2026-05-12T14:13:20.313Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending notification 'initialized'.
2026-05-12T14:13:20.313Z [DEBUG] LSP server plugin:gopls-lsp:go initialized
2026-05-12T14:13:20.313Z [DEBUG] LSP server instance started: plugin:gopls-lsp:go
2026-05-12T14:13:20.313Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending notification 'textDocument/didOpen'.
2026-05-12T14:13:20.314Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'textDocument/documentSymbol - (1)'.
2026-05-12T14:13:20.390Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'textDocument/documentSymbol - (1)' in 76ms.
2026-05-12T14:13:20.456Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received notification 'textDocument/publishDiagnostics'.
2026-05-12T14:13:30.787Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'textDocument/prepareCallHierarchy - (2)'.
2026-05-12T14:13:30.788Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'textDocument/prepareCallHierarchy - (3)'.
2026-05-12T14:13:30.788Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'textDocument/definition - (4)'.
2026-05-12T14:13:30.791Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'textDocument/prepareCallHierarchy - (2)' in 4ms.
2026-05-12T14:13:30.791Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'callHierarchy/outgoingCalls - (5)'.
2026-05-12T14:13:30.791Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'textDocument/prepareCallHierarchy - (3)' in 3ms.
2026-05-12T14:13:30.791Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'callHierarchy/incomingCalls - (6)'.
2026-05-12T14:13:30.791Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'textDocument/definition - (4)' in 3ms.
2026-05-12T14:13:30.803Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'callHierarchy/outgoingCalls - (5)' in 12ms.
2026-05-12T14:13:30.804Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'callHierarchy/incomingCalls - (6)' in 13ms.
2026-05-12T14:14:07.807Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending request 'shutdown - (7)'.
2026-05-12T14:14:07.819Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Received response 'shutdown - (7)' in 11ms.
2026-05-12T14:14:07.819Z [DEBUG] [LSP PROTOCOL plugin:gopls-lsp:go] Sending notification 'exit'.
2026-05-12T14:14:07.820Z [DEBUG] LSP client stopped for plugin:gopls-lsp:go
2026-05-12T14:14:07.820Z [DEBUG] LSP server instance stopped: plugin:gopls-lsp:go
2026-05-12T14:14:07.820Z [DEBUG] LSP server manager shut down successfully

Transcript — LSP tool call count

$ grep -c '"name":"LSP"' /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/transcripts/agent.jsonl

8

Transcript — LSP operations breakdown

$ grep -o '"operation":"[^"]*"' /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/transcripts/agent.jsonl | sort | uniq -c

   2 "operation":"documentSymbol"
   2 "operation":"goToDefinition"
   2 "operation":"incomingCalls"
   2 "operation":"outgoingCalls"

Transcript — LSP results

$ grep -oE '(Document symbols:|Found [0-9]+ (outgoing|incoming) call|Defined in|Call hierarchy item:)[^"]*' /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/transcripts/agent.jsonl

Document symbols:\nmain (Function) func() - Line 5\norchestrate (Function) func(name string) string - Line 10\nbuildGreeting (Function) func(name string) string - Line 15\ngetPrefix (Function) func() string - Line 20\nformatOutput (Function) func(s string) string - Line 24
Defined in target-repo/main.go:20:6
Found 2 outgoing calls:\n\ntarget-repo/main.go:\n  buildGreeting (Function) - Line 15 [called from: 11:14]\n  formatOutput (Function) - Line 24 [called from: 12:9]
Found 1 incoming call:\n\ntarget-repo/main.go:\n  orchestrate (Function) - Line 10 [calls at: 11:14]

Agent output — lsp-analysis.md

$ cat /tmp/fullsend-lsp-test/agent-lsp-callgraph-42943-1778595166/iteration-1/output/lsp-analysis.md

# LSP Call Graph Analysis

## Overview
This document presents the complete call chain analysis of the Go code using LSP tools, tracing execution flow from `main()` to `getPrefix()`.

## 1. Document Symbols (documentSymbol)

All functions found in `/tmp/workspace/target-repo/target-repo/main.go`:

- `main` (Function) - Line 5: `func()`
- `orchestrate` (Function) - Line 10: `func(name string) string`
- `buildGreeting` (Function) - Line 15: `func(name string) string`
- `getPrefix` (Function) - Line 20: `func() string`
- `formatOutput` (Function) - Line 24: `func(s string) string`

## 2. Outgoing Calls from `orchestrate` (Line 10, Character 6)

The `orchestrate` function makes 2 outgoing calls:

1. **buildGreeting** (Function) - Line 15
   - Called from: Line 11, Character 14

2. **formatOutput** (Function) - Line 24
   - Called from: Line 12, Character 9

## 3. Incoming Calls to `buildGreeting` (Line 15, Character 6)

The `buildGreeting` function has 1 incoming call:

1. **orchestrate** (Function) - Line 10
   - Calls at: Line 11, Character 14

## 4. Definition of `getPrefix()` (Line 16, Character 12)

The `getPrefix()` call inside `buildGreeting` is defined at:
- **Location**: `/tmp/workspace/target-repo/target-repo/main.go:20:6`

## Complete Call Chain: main() → getPrefix()

Based on the LSP analysis, the execution flow is:

main() [Line 5]
└─> orchestrate() [Line 10]
└─> buildGreeting() [Line 15]
└─> getPrefix() [Line 20]

Copy link
Copy Markdown
Contributor

@rh-hemartin rh-hemartin left a comment

Choose a reason for hiding this comment

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

A couple of things:

  • This needs some tests.
  • We need to scan plugins as we do with skills (raised by the review agent), even ifthey are JSON.
  • It would be nice to instruct the agent to install gopls using a skill within the plugin or something like that. We can explore this path in the near future.

Otherwise LGTM.

@github-actions
Copy link
Copy Markdown

fullsend review is working on this — view logs

Copy link
Copy Markdown

@fullsend-ai-review fullsend-ai-review Bot left a comment

Choose a reason for hiding this comment

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

See the review comment for full details.

@gklein
Copy link
Copy Markdown
Contributor Author

gklein commented May 14, 2026

A couple of things:

  • This needs some tests.

Tests: Added 17 unit tests covering buildPluginConfigs (single/multiple/no-LSP/invalid-JSON/empty-JSON/structure/empty-list), buildClaudeCommand plugin flags (multiple dirs, quote escaping, nil), harness validation (valid/invalid names, path resolution, traversal rejection, missing plugin), and ShouldScan for plugin files. Extracted buildPluginConfigs helper from bootstrapPlugins to make the JSON-building logic testable without a sandbox.

  • We need to scan plugins as we do with skills (raised by the review agent), even ifthey are JSON.

Plugin scanning: Both plugin.json and .lsp.json are now scanned via scanPipeline before upload, matching the skills scanning pattern. Also added both to the ScannableFiles map for repo-wide context scanning.

  • It would be nice to instruct the agent to install gopls using a skill within the plugin or something like that. We can explore this path in the near future.

Agreed, worth exploring.

Otherwise LGTM.

Security scanning for plugins:
- Scan both plugin.json and .lsp.json via scanPipeline before uploading
  to the sandbox, matching the existing skills scanning pattern.
- Add plugin.json and .lsp.json to the ScannableFiles map so they are
  included in repo-wide context scanning.

Error handling:
- Check the error return from tmp.Write(data) in the config file upload
  loop. Previously ignored, a failed write would silently upload a
  corrupt or empty config to the sandbox. Now follows the same
  error-check-close-remove pattern used by bootstrapSecurityHooks.

Conditional PATH:
- Gate the /usr/local/go/bin PATH addition on len(h.Plugins) > 0.
  Previously unconditional for all agents, it now only applies when
  plugins are configured since only gopls requires it.

Testability and test coverage:
- Extract buildPluginConfigs helper from bootstrapPlugins, separating
  pure JSON-building logic from sandbox I/O for unit testability.
- Add 17 unit tests across 3 packages:
  - buildPluginConfigs: single plugin, multiple plugins, no .lsp.json,
    invalid .lsp.json, empty .lsp.json, config structure, empty list
  - buildClaudeCommand: multiple plugin dirs, quote escaping, nil plugins
  - Harness: valid/invalid plugin names, path resolution, traversal
    rejection, missing plugin directory
  - ShouldScan: plugin.json, Plugin.json, .lsp.json

Code quality:
- Move scanPipeline nil-check outside the plugin loop to avoid redundant
  evaluation and reduce nesting depth.
- Pass mktBase to buildPluginConfigs instead of recomputing it, avoiding
  a potential silent divergence between directory creation and config
  file paths.
- Fix non-critical findings warning message to use "in %s" preposition,
  consistent with the critical findings branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gklein gklein force-pushed the feat/gopls-lsp-plugin branch from fdcf618 to 1ffc74a Compare May 14, 2026 05:52
@github-actions
Copy link
Copy Markdown

fullsend review is working on this — view logs

@rh-hemartin rh-hemartin added this pull request to the merge queue May 14, 2026
Merged via the queue into fullsend-ai:main with commit d0b6252 May 14, 2026
39 checks passed
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