Skip to content
Open
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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,13 @@ ACP providers are configured in the `acp_providers` section of your configuratio
}
```

### Environment Variables Handling

When spawning an ACP agent, `avante.nvim` handles environment variables as follows:

1. **Inheritance**: The agent inherits all environment variables from the current Neovim process.
2. **Overrides**: Environment variables defined in the `env` table of the ACP provider configuration will override inherited values. Setting a variable to `vim.NIL` will remove it from the environment.

### Prerequisites

Before using ACP agents, ensure you have the required tools installed:
Expand Down Expand Up @@ -1707,4 +1714,4 @@ avante.nvim is licensed under the Apache 2.0 License. For more details, please r
<img alt="NebulaGraph Data Intelligence Suite(ngdi)" src="https://api.star-history.com/svg?repos=yetone/avante.nvim&type=Date">
</picture>
</a>
</p>
</p>
26 changes: 17 additions & 9 deletions lua/avante/libs/acp_client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -355,20 +355,28 @@ function ACPClient:_create_stdio_transport()
end

local args = vim.deepcopy(self.config.args or {})
local env = self.config.env

-- Start with system environment and override with config env
local final_env = {}

local path = vim.fn.getenv("PATH")
if path then final_env[#final_env + 1] = "PATH=" .. path end
-- Build the final environment map, inheriting from the current process
-- and allowing overrides from the plugin configuration.
local inherited_env = vim.fn.environ()
local override_env = self.config.env

if env then
for k, v in pairs(env) do
final_env[#final_env + 1] = k .. "=" .. v
if override_env then
for k, v in pairs(override_env) do
if v == vim.NIL then
inherited_env[k] = nil
else
inherited_env[k] = v
end
end
end

-- Convert map to list of "KEY=VALUE" strings for uv.spawn
local final_env = {}
for k, v in pairs(inherited_env) do
final_env[#final_env + 1] = k .. "=" .. v
end

---@diagnostic disable-next-line: missing-fields
local handle, pid = uv.spawn(self.config.command, {
args = args,
Expand Down
174 changes: 174 additions & 0 deletions tests/libs/acp_client_env_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
local ACPClient = require("avante.libs.acp_client")
local stub = require("luassert.stub")

describe("ACPClient Environment Handling", function()
local schedule_stub
local environ_stub
local spawn_stub
local uv = vim.uv or vim.loop

before_each(function()
schedule_stub = stub(vim, "schedule")
schedule_stub.invokes(function(fn) fn() end)

environ_stub = stub(vim.fn, "environ")
spawn_stub = stub(uv, "spawn")
end)

after_each(function()
schedule_stub:revert()
environ_stub:revert()
spawn_stub:revert()
end)

it("should inherit all environment variables when no overrides are provided", function()
-- Mock environment
environ_stub.returns({
USER = "testuser",
HOME = "/home/testuser",
PATH = "/usr/bin:/bin"
})

-- Mock successful spawn
spawn_stub.returns({}, 1234)

local config = {
transport_type = "stdio",
command = "test-agent",
args = {},
env = nil -- No overrides
}

local client = ACPClient:new(config)
client:connect(function() end)

assert.stub(spawn_stub).was_called(1)
local call_args = spawn_stub.calls[1].refs
local options = call_args[2]
local env_list = options.env

-- Verify env list contains all inherited variables
local env_map = {}
for _, item in ipairs(env_list) do
local k, v = item:match("([^=]+)=(.*)")
env_map[k] = v
end

assert.equals("testuser", env_map["USER"])
assert.equals("/home/testuser", env_map["HOME"])
assert.equals("/usr/bin:/bin", env_map["PATH"])
end)

it("should override existing environment variables", function()
-- Mock environment
environ_stub.returns({
USER = "testuser",
HOME = "/home/testuser",
PATH = "/usr/bin:/bin"
})

-- Mock successful spawn
spawn_stub.returns({}, 1234)

local config = {
transport_type = "stdio",
command = "test-agent",
args = {},
env = {
HOME = "/home/override"
}
}

local client = ACPClient:new(config)
client:connect(function() end)

assert.stub(spawn_stub).was_called(1)
local call_args = spawn_stub.calls[1].refs
local options = call_args[2]
local env_list = options.env

local env_map = {}
for _, item in ipairs(env_list) do
local k, v = item:match("([^=]+)=(.*)")
env_map[k] = v
end

assert.equals("testuser", env_map["USER"]) -- Inherited
assert.equals("/home/override", env_map["HOME"]) -- Overridden
assert.equals("/usr/bin:/bin", env_map["PATH"]) -- Inherited
end)

it("should add new environment variables", function()
-- Mock environment
environ_stub.returns({
USER = "testuser",
})

-- Mock successful spawn
spawn_stub.returns({}, 1234)

local config = {
transport_type = "stdio",
command = "test-agent",
args = {},
env = {
NEW_VAR = "new_value"
}
}

local client = ACPClient:new(config)
client:connect(function() end)

assert.stub(spawn_stub).was_called(1)
local call_args = spawn_stub.calls[1].refs
local options = call_args[2]
local env_list = options.env

local env_map = {}
for _, item in ipairs(env_list) do
local k, v = item:match("([^=]+)=(.*)")
env_map[k] = v
end

assert.equals("testuser", env_map["USER"])
assert.equals("new_value", env_map["NEW_VAR"])
end)

it("should delete inherited environment variables when overridden with nil", function()
-- Mock environment
environ_stub.returns({
USER = "testuser",
HOME = "/home/testuser",
TO_BE_REMOVED = "remove_me"
})

-- Mock successful spawn
spawn_stub.returns({}, 1234)

local config = {
transport_type = "stdio",
command = "test-agent",
args = {},
env = {
TO_BE_REMOVED = vim.NIL
}
}

local client = ACPClient:new(config)
client:connect(function() end)

assert.stub(spawn_stub).was_called(1)
local call_args = spawn_stub.calls[1].refs
local options = call_args[2]
local env_list = options.env

local env_map = {}
for _, item in ipairs(env_list) do
local k, v = item:match("([^=]+)=(.*)")
env_map[k] = v
end

assert.equals("testuser", env_map["USER"])
assert.is_nil(env_map["TO_BE_REMOVED"])
end)
end)