Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/reference/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ Notes:
- `--allow-shell` enables shell tool use broadly, subject to the trust gate.
- `--allow-shell-in-workdir` is narrower: it allows shell only when cwd is omitted or remains under the current workdir.

### LSP Tools

- `--lsp-provider <rust|typescript>`
- `--lsp-command <PATH>`

When `--lsp-provider` is set, LocalAgent exposes read-only built-in tools:

- `lsp.diagnostics`
- `lsp.document_symbols`
- `lsp.goto_definition`
- `lsp.find_references`
- `lsp.hover`

These tools are workdir-scoped, use the configured language server command, and are classified as filesystem-read tools for gate/audit purposes.

* `Evidence: src/cli_args.rs#LspProviderKind`
* `Evidence: src/tools/catalog.rs#builtin_tools_enabled_with_lsp`
* `Evidence: src/tools/exec_lsp.rs`

### Execution Target

- `--exec-target <host|docker>` (default: `host`)
Expand Down
13 changes: 13 additions & 0 deletions src/agent_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ pub(crate) async fn run_agent_with_ui<P: ModelProvider>(
},
unsafe_bypass_allow_flags: args.unsafe_bypass_allow_flags,
tool_args_strict: resolved_settings.tool_args_strict,
lsp: args
.lsp_provider
.map(|provider| crate::tools::LspToolRuntime {
provider: match provider {
crate::cli_args::LspProviderKind::Rust => {
crate::tools::LspToolProvider::Rust
}
crate::cli_args::LspProviderKind::Typescript => {
crate::tools::LspToolProvider::Typescript
}
},
command: args.lsp_command.clone(),
}),
exec_target_kind: resolved_target_kind,
exec_target,
},
Expand Down
49 changes: 49 additions & 0 deletions src/agent_tests.rs

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/bin/lsp_stub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ fn main() -> Result<()> {
}),
)?;
}
Some("textDocument/hover") => {
write_lsp_message(
&mut writer,
&json!({
"jsonrpc": "2.0",
"id": message.get("id").cloned().unwrap_or(Value::Null),
"result": {
"contents": {
"kind": "markdown",
"value": "const value: number"
}
}
}),
)?;
}
Some("shutdown") => {
write_lsp_message(
&mut writer,
Expand Down
2 changes: 2 additions & 0 deletions src/cli_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,8 @@ pub(crate) enum RunOutputMode {

#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum LspProviderKind {
Rust,

Typescript,
}

Expand Down
1 change: 1 addition & 0 deletions src/eval/runner_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ pub(crate) async fn run_single(
max_read_bytes: if config.no_limits { 0 } else { 200_000 },
unsafe_bypass_allow_flags: config.unsafe_bypass_allow_flags,
tool_args_strict: config.tool_args_strict,
lsp: None,
exec_target_kind: ExecTargetKind::Host,
exec_target: std::sync::Arc::new(HostTarget),
},
Expand Down
1 change: 1 addition & 0 deletions src/lsp_context_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) fn resolve_default_lsp_context(
) -> Result<Option<ResolvedLspContext>> {
match args.lsp_provider {
None => Ok(None),
Some(LspProviderKind::Rust) => Ok(None),
Some(LspProviderKind::Typescript) => {
let provider = TypescriptLspContextProvider::new(args.lsp_command.clone());
match resolve_lsp_context(&args.workdir, &provider, limits) {
Expand Down
4 changes: 2 additions & 2 deletions src/repro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::store::{config_hash_hex, sha256_hex, stable_path_string, RunRecord};
use crate::tools::builtin_tools_enabled;
use crate::tools::builtin_tools_enabled_with_lsp;

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize, Deserialize)]
pub enum ReproMode {
Expand Down Expand Up @@ -327,7 +327,7 @@ pub fn verify_run_record(record: &RunRecord, strict: bool) -> anyhow::Result<Rep

checks.push(verify_hooks_config_hash(record)?);

let builtin = builtin_tools_enabled(true, true);
let builtin = builtin_tools_enabled_with_lsp(true, true, true);
let mut actual_schema_map = crate::store::tool_schema_hash_hex_map(&builtin);
let mut mcp_live_snapshot_note = None;
let mut mcp_live_hash = String::from("unavailable");
Expand Down
5 changes: 3 additions & 2 deletions src/run_prep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::mcp::registry::McpRegistry;
use crate::providers::ModelProvider;
use crate::qualification;
use crate::store;
use crate::tools::builtin_tools_enabled;
use crate::tools::builtin_tools_enabled_with_lsp;
use crate::trust::policy::Policy;
use crate::types;
use crate::RunArgs;
Expand Down Expand Up @@ -52,9 +52,10 @@ pub(crate) async fn prepare_tools_and_qualification<P: ModelProvider>(
mcp_registry,
policy_for_exposure,
} = input;
let mut all_tools = builtin_tools_enabled(
let mut all_tools = builtin_tools_enabled_with_lsp(
args.enable_write_tools,
args.allow_shell || args.allow_shell_in_workdir,
args.lsp_provider.is_some(),
);
let mut mcp_tool_snapshot: Vec<store::McpToolSnapshotEntry> = Vec::new();
if let Some(reg) = mcp_registry {
Expand Down
25 changes: 24 additions & 1 deletion src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::types::{Message, SideEffects, ToolCall};
mod catalog;
mod envelope;
mod exec_fs;
mod exec_lsp;
mod exec_plan;
mod exec_shell;
mod exec_support;
Expand All @@ -19,7 +20,7 @@ mod observation;
mod schema;

pub(crate) use catalog::normalize_builtin_tool_args;
pub use catalog::{builtin_tools_enabled, tool_side_effects};
pub use catalog::{builtin_tools_enabled, builtin_tools_enabled_with_lsp, tool_side_effects};
pub use envelope::{
envelope_to_message, invalid_args_tool_message, to_tool_result_envelope,
to_tool_result_envelope_with_error,
Expand Down Expand Up @@ -53,6 +54,18 @@ impl ToolArgsStrict {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LspToolProvider {
Rust,
Typescript,
}

#[derive(Debug, Clone)]
pub struct LspToolRuntime {
pub provider: LspToolProvider,
pub command: Option<PathBuf>,
}

#[derive(Clone)]
pub struct ToolRuntime {
pub workdir: PathBuf,
Expand All @@ -63,6 +76,7 @@ pub struct ToolRuntime {
pub max_read_bytes: usize,
pub unsafe_bypass_allow_flags: bool,
pub tool_args_strict: ToolArgsStrict,
pub lsp: Option<LspToolRuntime>,
pub exec_target_kind: ExecTargetKind,
pub exec_target: Arc<dyn ExecTarget>,
}
Expand Down Expand Up @@ -144,6 +158,8 @@ pub enum ToolErrorCode {
ShellExecNonZeroExit,
ShellExecTimeout,
ShellExecTimeoutUnsupported,
LspUnavailable,
LspRequestFailed,
}

impl ToolErrorCode {
Expand All @@ -164,6 +180,8 @@ impl ToolErrorCode {
Self::ShellExecNonZeroExit => "shell_exec_non_zero_exit",
Self::ShellExecTimeout => "shell_exec_timeout",
Self::ShellExecTimeoutUnsupported => "shell_exec_timeout_unsupported",
Self::LspUnavailable => "lsp_unavailable",
Self::LspRequestFailed => "lsp_request_failed",
}
}
}
Expand Down Expand Up @@ -216,6 +234,11 @@ pub async fn execute_tool_streaming(
"read_file" => exec_fs::run_read_file(rt, &normalized_args).await,
"glob" => exec_fs::run_glob(rt, &normalized_args).await,
"grep" => exec_fs::run_grep(rt, &normalized_args).await,
"lsp.diagnostics"
| "lsp.document_symbols"
| "lsp.goto_definition"
| "lsp.find_references"
| "lsp.hover" => exec_lsp::run_lsp_tool(rt, &tc.name, &normalized_args).await,
"update_plan" => exec_plan::run_update_plan(rt, &normalized_args).await,
"shell" => exec_shell::run_shell(rt, &normalized_args, shell_stream).await,
"write_file" => exec_write::run_write_file(rt, &normalized_args).await,
Expand Down
90 changes: 89 additions & 1 deletion src/tools/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ use crate::types::{SideEffects, ToolDef};

pub fn tool_side_effects(tool_name: &str) -> SideEffects {
match tool_name {
"list_dir" | "read_file" | "glob" | "grep" => SideEffects::FilesystemRead,
"list_dir"
| "read_file"
| "glob"
| "grep"
| "lsp.diagnostics"
| "lsp.document_symbols"
| "lsp.goto_definition"
| "lsp.find_references"
| "lsp.hover" => SideEffects::FilesystemRead,
"update_plan" => SideEffects::None,
"shell" => SideEffects::ShellExec,
"write_file" | "apply_patch" | "edit" | "str_replace" => SideEffects::FilesystemWrite,
Expand All @@ -15,6 +23,14 @@ pub fn tool_side_effects(tool_name: &str) -> SideEffects {
}

pub fn builtin_tools_enabled(enable_write_tools: bool, enable_shell_tool: bool) -> Vec<ToolDef> {
builtin_tools_enabled_with_lsp(enable_write_tools, enable_shell_tool, false)
}

pub fn builtin_tools_enabled_with_lsp(
enable_write_tools: bool,
enable_shell_tool: bool,
enable_lsp_tools: bool,
) -> Vec<ToolDef> {
let mut tools = vec![
ToolDef {
name: "list_dir".to_string(),
Expand Down Expand Up @@ -91,6 +107,78 @@ pub fn builtin_tools_enabled(enable_write_tools: bool, enable_shell_tool: bool)
side_effects: SideEffects::None,
},
];
if enable_lsp_tools {
tools.push(ToolDef {
name: "lsp.diagnostics".to_string(),
description: "Read current language-server diagnostics for the workspace or a workdir-relative source file. Read-only.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"path":{"type":"string"},
"max_results":{"type":"integer","minimum":1,"maximum":1000}
}
}),
side_effects: SideEffects::FilesystemRead,
});
tools.push(ToolDef {
name: "lsp.document_symbols".to_string(),
description: "Read document symbols for a workdir-relative Rust or TypeScript-family source file. Read-only.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"path":{"type":"string"},
"max_results":{"type":"integer","minimum":1,"maximum":1000}
},
"required":["path"]
}),
side_effects: SideEffects::FilesystemRead,
});
tools.push(ToolDef {
name: "lsp.goto_definition".to_string(),
description: "Read definition locations for a symbol position in a workdir-relative source file. Lines and columns are 1-based. Read-only.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"path":{"type":"string"},
"line":{"type":"integer","minimum":1},
"column":{"type":"integer","minimum":1},
"max_results":{"type":"integer","minimum":1,"maximum":1000}
},
"required":["path","line","column"]
}),
side_effects: SideEffects::FilesystemRead,
});
tools.push(ToolDef {
name: "lsp.find_references".to_string(),
description: "Read reference locations for a symbol position in a workdir-relative source file. Lines and columns are 1-based. Read-only.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"path":{"type":"string"},
"line":{"type":"integer","minimum":1},
"column":{"type":"integer","minimum":1},
"include_declaration":{"type":"boolean"},
"max_results":{"type":"integer","minimum":1,"maximum":1000}
},
"required":["path","line","column"]
}),
side_effects: SideEffects::FilesystemRead,
});
tools.push(ToolDef {
name: "lsp.hover".to_string(),
description: "Read hover text for a symbol position in a workdir-relative source file. Lines and columns are 1-based. Read-only.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"path":{"type":"string"},
"line":{"type":"integer","minimum":1},
"column":{"type":"integer","minimum":1}
},
"required":["path","line","column"]
}),
side_effects: SideEffects::FilesystemRead,
});
}
if enable_shell_tool {
tools.push(ToolDef {
name: "shell".to_string(),
Expand Down
Loading
Loading