Compare commits
6
Commits
c102da5665
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b37eb4602 | ||
|
|
40996a06fe | ||
|
|
00a21ee777 | ||
|
|
016f2654e4 | ||
|
|
9a1d00f5e6 | ||
|
|
a14c768f9a |
@@ -1,3 +1,9 @@
|
||||
## v0.10.1 (2026-08-31)
|
||||
|
||||
### Fix
|
||||
|
||||
- make binary shims for custom tools cross-device compatible so users can't accidentally break sandboxes via `sbx cp ~/.config/coyote <sbx-name>:/home/agent/.config`
|
||||
|
||||
## v0.10.0 (2026-08-29)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
Generated
+9
-9
@@ -1663,7 +1663,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "coyote-ai"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"ansi_colours",
|
||||
"anyhow",
|
||||
@@ -2956,9 +2956,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
@@ -4881,9 +4881,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr3"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7"
|
||||
checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4891,9 +4891,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error3"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50"
|
||||
checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr3",
|
||||
"proc-macro2",
|
||||
@@ -6887,9 +6887,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-language"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
|
||||
checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-python"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coyote-ai"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "The batteries-included runtime for LLMs"
|
||||
|
||||
@@ -27,7 +27,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
||||
* [AIChat Migration Guide](https://github.com/Dark-Alex-17/coyote/wiki/AIChat-Migration): Coming from AIChat? Follow the migration guide to get started.
|
||||
* [Installation](#install): Install Coyote
|
||||
* [Getting Started](#getting-started): Get started with Coyote by doing first-run setup steps.
|
||||
* [Sharing Configurations](https://github.com/Dark-Alex-17/coyote/wiki/Sharing-Configurations): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
|
||||
* [Bundles](https://github.com/Dark-Alex-17/coyote/wiki/Bundles): Install bundles of agents, roles, skills, macros, tools, and MCP servers from any git repo, and share your own. Bundles are Coyote's equivalent of plugins in other CLI agents.
|
||||
* [REPL](https://github.com/Dark-Alex-17/coyote/wiki/REPL): Interactive Read-Eval-Print Loop for conversational interactions with LLMs and Coyote.
|
||||
* [Custom REPL Prompt](https://github.com/Dark-Alex-17/coyote/wiki/REPL-Prompt): Customize the REPL prompt to provide useful contextual information.
|
||||
* [Vault](https://github.com/Dark-Alex-17/coyote/wiki/Vault): Securely store and manage sensitive information such as API keys and credentials.
|
||||
@@ -43,8 +43,8 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
||||
* Models interact with each server through a compact set of capability-gated meta-tools: `mcp_search`/`mcp_describe` for discovery across tools, resources, and prompts, `mcp_invoke` for tool calls, `mcp_read` for paged and regex-filterable resource reads, and `mcp_prompt` for server-defined prompts. Binary content is spilled to disk instead of inlined, and oversized tool results are bounded before they reach the model.
|
||||
* Invoke server prompts yourself with `.prompt <server> <name> [key=value ...]` in the REPL, with live staged tab-completion (servers, then prompt names, then `key=` arguments), and discover them with `.list prompts`.
|
||||
* [Macros](https://github.com/Dark-Alex-17/coyote/wiki/Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). Macros are Coyote's custom commands: invoke any macro directly by name (e.g. `.review main`), with tab-completion, right alongside the built-in REPL commands.
|
||||
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends — by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
|
||||
* Commit project-specific macros to `.coyote/macros/` in your repo — they shadow same-named global macros (opt out with `--no-workspace-macros`).
|
||||
* Give a macro a `description` (shown in `.list macros` and completions) and set `isolated: false` to run its steps on the live session, exactly as if you typed them. Note that non-isolated steps are recorded in the session, and mutating steps (`.role`, `.model`, ...) persist after the macro ends, by design. Steps are fail-fast: an error aborts the remaining steps, but completed steps' effects remain. A non-isolated macro step cannot invoke another macro, and a `.exit` step never exits the REPL.
|
||||
* Commit project-specific macros to `.coyote/macros/` in your repo. They shadow same-named global macros (opt out with `--no-workspace-macros`).
|
||||
* Pass variables positionally or by name: leading `name=value` args set declared variables directly (letting earlier variables keep their defaults), and remaining args fill the rest in order. Tab completion after a macro name lists each variable with its description and default.
|
||||
* Scope which macros are invocable with `enabled_macros` in the global config, a role, an agent, or a session (most specific wins; an empty list disables all macros), and toggle at runtime with `.macro enable|disable <name>`.
|
||||
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||
|
||||
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
|
||||
_ensure_cwd_venv()
|
||||
|
||||
|
||||
def resolve_dir(env_name, default_path):
|
||||
"""Resolve a directory at run time.
|
||||
|
||||
Prefer the override env var when set, otherwise fall back to the default
|
||||
path derived from this script's own location, so the shim keeps working
|
||||
when the config dir moves or is shared across environments with different
|
||||
home directories.
|
||||
"""
|
||||
value = os.environ.get(env_name)
|
||||
if value:
|
||||
return value
|
||||
return os.path.normpath(default_path)
|
||||
|
||||
|
||||
def main():
|
||||
(agent_func, raw_data) = parse_argv()
|
||||
agent_data = parse_raw_data(raw_data)
|
||||
|
||||
root_dir = "{config_dir}"
|
||||
setup_env(root_dir, agent_func, raw_data)
|
||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
agent_dir = os.path.normpath(os.path.join(self_dir, ".."))
|
||||
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
|
||||
setup_env(root_dir, agent_dir, agent_func, raw_data)
|
||||
|
||||
agent_tools_path = os.path.join(root_dir, "agents/{agent_name}/tools.py")
|
||||
agent_tools_path = os.path.join(agent_dir, "tools.py")
|
||||
run(agent_tools_path, agent_func, agent_data)
|
||||
|
||||
|
||||
@@ -65,12 +81,12 @@ def parse_argv():
|
||||
return agent_func, agent_data
|
||||
|
||||
|
||||
def setup_env(root_dir, agent_func, raw_data):
|
||||
def setup_env(root_dir, agent_dir, agent_func, raw_data):
|
||||
load_env(os.path.join(root_dir, ".env"))
|
||||
os.environ["LLM_ROOT_DIR"] = root_dir
|
||||
os.environ["LLM_AGENT_NAME"] = "{agent_name}"
|
||||
os.environ["LLM_AGENT_FUNC"] = agent_func
|
||||
os.environ["LLM_AGENT_ROOT_DIR"] = os.path.join(root_dir, "agents", "{agent_name}")
|
||||
os.environ["LLM_AGENT_ROOT_DIR"] = agent_dir
|
||||
os.environ["LLM_AGENT_CACHE_DIR"] = os.path.join(root_dir, "cache", "{agent_name}")
|
||||
os.environ["LLM_AGENT_RAW_JSON"] = raw_data
|
||||
|
||||
|
||||
@@ -5,13 +5,30 @@
|
||||
set -e
|
||||
|
||||
main() {
|
||||
root_dir="{config_dir}"
|
||||
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
agent_dir="$(cd "$self_dir/.." && pwd)"
|
||||
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
|
||||
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
|
||||
parse_argv "$@"
|
||||
setup_env
|
||||
tools_path="$root_dir/agents/{agent_name}/tools.sh"
|
||||
tools_path="$agent_dir/tools.sh"
|
||||
run
|
||||
}
|
||||
|
||||
# Resolve a directory at run time: prefer the override env var ($1) when set,
|
||||
# otherwise fall back to the default path ($2) derived from this script's own
|
||||
# location, so the shim keeps working when the config dir moves or is shared
|
||||
# across environments with different home directories.
|
||||
resolve_dir() {
|
||||
local override
|
||||
override="$(printenv "$1" 2>/dev/null || true)"
|
||||
if [[ -n "$override" ]]; then
|
||||
echo "$override"
|
||||
else
|
||||
(cd "$2" 2>/dev/null && pwd) || echo "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
agent_func="$1"
|
||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||
@@ -29,9 +46,9 @@ setup_env() {
|
||||
export LLM_ROOT_DIR="$root_dir"
|
||||
export LLM_AGENT_NAME="{agent_name}"
|
||||
export LLM_AGENT_FUNC="$agent_func"
|
||||
export LLM_AGENT_ROOT_DIR="$LLM_ROOT_DIR/agents/{agent_name}"
|
||||
export LLM_AGENT_ROOT_DIR="$agent_dir"
|
||||
export LLM_AGENT_CACHE_DIR="$LLM_ROOT_DIR/cache/{agent_name}"
|
||||
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
|
||||
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
|
||||
export LLM_AGENT_RAW_JSON="$agent_data"
|
||||
}
|
||||
|
||||
@@ -59,6 +76,10 @@ run() {
|
||||
die "error: no JSON data"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tools_path" ]]; then
|
||||
die "error: agent tools script not found: $tools_path"
|
||||
fi
|
||||
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tools_path="$(cygpath -w "$tools_path")"
|
||||
@@ -122,4 +143,3 @@ die() {
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
|
||||
@@ -3,17 +3,38 @@
|
||||
// Usage: ./{agent_name}.ts <agent-func> <agent-data>
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath, pathToFileURL } from "url";
|
||||
|
||||
function selfDir(): string {
|
||||
if (typeof __dirname !== "undefined") {
|
||||
return __dirname;
|
||||
}
|
||||
return dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
// Resolve a directory at run time: prefer the override env var when set,
|
||||
// otherwise fall back to the default path derived from this script's own
|
||||
// location, so the shim keeps working when the config dir moves or is shared
|
||||
// across environments with different home directories.
|
||||
function resolveDir(envName: string, defaultPath: string): string {
|
||||
const value = process.env[envName];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
return resolve(defaultPath);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { agentFunc, rawData } = parseArgv();
|
||||
const agentData = parseRawData(rawData);
|
||||
|
||||
const configDir = "{config_dir}";
|
||||
setupEnv(configDir, agentFunc, rawData);
|
||||
const binDir = selfDir();
|
||||
const agentDir = resolve(binDir, "..");
|
||||
const configDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
|
||||
setupEnv(configDir, agentDir, agentFunc, rawData);
|
||||
|
||||
const agentToolsPath = join(configDir, "agents", "{agent_name}", "tools.ts");
|
||||
const agentToolsPath = join(agentDir, "tools.ts");
|
||||
await run(agentToolsPath, agentFunc, agentData);
|
||||
}
|
||||
|
||||
@@ -48,12 +69,17 @@ function parseArgv(): { agentFunc: string; rawData: string } {
|
||||
return { agentFunc, rawData: agentData };
|
||||
}
|
||||
|
||||
function setupEnv(configDir: string, agentFunc: string, rawData: string): void {
|
||||
function setupEnv(
|
||||
configDir: string,
|
||||
agentDir: string,
|
||||
agentFunc: string,
|
||||
rawData: string,
|
||||
): void {
|
||||
loadEnv(join(configDir, ".env"));
|
||||
process.env["LLM_ROOT_DIR"] = configDir;
|
||||
process.env["LLM_AGENT_NAME"] = "{agent_name}";
|
||||
process.env["LLM_AGENT_FUNC"] = agentFunc;
|
||||
process.env["LLM_AGENT_ROOT_DIR"] = join(configDir, "agents", "{agent_name}");
|
||||
process.env["LLM_AGENT_ROOT_DIR"] = agentDir;
|
||||
process.env["LLM_AGENT_CACHE_DIR"] = join(configDir, "cache", "{agent_name}");
|
||||
process.env["LLM_AGENT_RAW_JSON"] = rawData;
|
||||
}
|
||||
|
||||
@@ -27,14 +27,30 @@ def _ensure_cwd_venv():
|
||||
_ensure_cwd_venv()
|
||||
|
||||
|
||||
def resolve_dir(env_name, default_path):
|
||||
"""Resolve a directory at run time.
|
||||
|
||||
Prefer the override env var when set, otherwise fall back to the default
|
||||
path derived from this script's own location, so the shim keeps working
|
||||
when the config dir moves or is shared across environments with different
|
||||
home directories.
|
||||
"""
|
||||
value = os.environ.get(env_name)
|
||||
if value:
|
||||
return value
|
||||
return os.path.normpath(default_path)
|
||||
|
||||
|
||||
def main():
|
||||
raw_data = parse_argv()
|
||||
tool_data = parse_raw_data(raw_data)
|
||||
|
||||
root_dir = "{root_dir}"
|
||||
self_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = resolve_dir("{root_dir_env}", os.path.join(self_dir, "{root_dir_rel}"))
|
||||
functions_dir = resolve_dir("{functions_dir_env}", os.path.join(self_dir, "{functions_dir_rel}"))
|
||||
setup_env(root_dir, raw_data)
|
||||
|
||||
tool_path = "{tool_path}.py"
|
||||
tool_path = os.path.join(functions_dir, "tools", "{function_name}.py")
|
||||
run(tool_path, "run", tool_data)
|
||||
|
||||
|
||||
|
||||
@@ -5,13 +5,29 @@
|
||||
set -e
|
||||
|
||||
main() {
|
||||
root_dir="{root_dir}"
|
||||
self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root_dir="$(resolve_dir "{root_dir_env}" "$self_dir/{root_dir_rel}")"
|
||||
functions_dir="$(resolve_dir "{functions_dir_env}" "$self_dir/{functions_dir_rel}")"
|
||||
parse_argv "$@"
|
||||
setup_env
|
||||
tool_path="{tool_path}.sh"
|
||||
tool_path="$functions_dir/tools/{function_name}.sh"
|
||||
run
|
||||
}
|
||||
|
||||
# Resolve a directory at run time: prefer the override env var ($1) when set,
|
||||
# otherwise fall back to the default path ($2) derived from this script's own
|
||||
# location, so the shim keeps working when the config dir moves or is shared
|
||||
# across environments with different home directories.
|
||||
resolve_dir() {
|
||||
local override
|
||||
override="$(printenv "$1" 2>/dev/null || true)"
|
||||
if [[ -n "$override" ]]; then
|
||||
echo "$override"
|
||||
else
|
||||
(cd "$2" 2>/dev/null && pwd) || echo "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
parse_argv() {
|
||||
if [[ -n "$LLM_TOOL_DATA_FILE" ]] && [[ -f "$LLM_TOOL_DATA_FILE" ]]; then
|
||||
tool_data="$(cat "$LLM_TOOL_DATA_FILE")"
|
||||
@@ -28,7 +44,7 @@ setup_env() {
|
||||
export LLM_ROOT_DIR="$root_dir"
|
||||
export LLM_TOOL_NAME="{function_name}"
|
||||
export LLM_TOOL_CACHE_DIR="$LLM_ROOT_DIR/cache/{function_name}"
|
||||
export LLM_PROMPT_UTILS_FILE="{prompt_utils_file}"
|
||||
export LLM_PROMPT_UTILS_FILE="$functions_dir/utils/prompt-utils.sh"
|
||||
export LLM_TOOL_RAW_JSON="$tool_data"
|
||||
}
|
||||
|
||||
@@ -56,6 +72,10 @@ run() {
|
||||
die "error: no JSON data"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$tool_path" ]]; then
|
||||
die "error: tool script not found: $tool_path"
|
||||
fi
|
||||
|
||||
if [[ "$OS" == "Windows_NT" ]]; then
|
||||
set -o igncr
|
||||
tool_path="$(cygpath -w "$tool_path")"
|
||||
|
||||
@@ -3,17 +3,41 @@
|
||||
// Usage: ./{function_name}.ts <tool-data>
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath, pathToFileURL } from "url";
|
||||
|
||||
function selfDir(): string {
|
||||
if (typeof __dirname !== "undefined") {
|
||||
return __dirname;
|
||||
}
|
||||
return dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
// Resolve a directory at run time: prefer the override env var when set,
|
||||
// otherwise fall back to the default path derived from this script's own
|
||||
// location, so the shim keeps working when the config dir moves or is shared
|
||||
// across environments with different home directories.
|
||||
function resolveDir(envName: string, defaultPath: string): string {
|
||||
const value = process.env[envName];
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
return resolve(defaultPath);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const rawData = parseArgv();
|
||||
const toolData = parseRawData(rawData);
|
||||
|
||||
const rootDir = "{root_dir}";
|
||||
const binDir = selfDir();
|
||||
const rootDir = resolveDir("{root_dir_env}", join(binDir, "{root_dir_rel}"));
|
||||
const functionsDir = resolveDir(
|
||||
"{functions_dir_env}",
|
||||
join(binDir, "{functions_dir_rel}"),
|
||||
);
|
||||
setupEnv(rootDir, rawData);
|
||||
|
||||
const toolPath = "{tool_path}.ts";
|
||||
const toolPath = join(functionsDir, "tools", "{function_name}.ts");
|
||||
await run(toolPath, "run", toolData);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ description: >
|
||||
CLI & REPL mode, RAG, AI tools & agents, MCP servers, skills, and macros.
|
||||
|
||||
sandbox:
|
||||
image: 'darkalex17/coyote:v0.10.0'
|
||||
image: 'darkalex17/coyote:v0.10.1'
|
||||
entrypoint: ['bash', '-lc', 'exec /home/agent/.cargo/bin/coyote']
|
||||
|
||||
permissions:
|
||||
|
||||
+130
-77
@@ -1175,6 +1175,35 @@ impl Functions {
|
||||
Self::build_binaries(name, language, BinaryType::Agent, custom_runtime.as_deref())
|
||||
}
|
||||
|
||||
fn render_shim_template(
|
||||
content_template: &str,
|
||||
binary_name: &str,
|
||||
binary_type: &BinaryType,
|
||||
) -> String {
|
||||
let functions_dir_env = get_env_name("functions_dir");
|
||||
match binary_type {
|
||||
BinaryType::Tool(None) => content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace("{root_dir_env}", &functions_dir_env)
|
||||
.replace("{root_dir_rel}", "..")
|
||||
.replace("{functions_dir_rel}", ".."),
|
||||
BinaryType::Tool(Some(agent_name)) => content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace(
|
||||
"{root_dir_env}",
|
||||
&format!("{}_DATA_DIR", normalize_env_name(agent_name)),
|
||||
)
|
||||
.replace("{root_dir_rel}", "..")
|
||||
.replace("{functions_dir_rel}", "../../../functions"),
|
||||
BinaryType::Agent => content_template
|
||||
.replace("{agent_name}", binary_name)
|
||||
.replace("{root_dir_env}", &get_env_name("config_dir"))
|
||||
.replace("{root_dir_rel}", "../../..")
|
||||
.replace("{functions_dir_rel}", "../../../functions"),
|
||||
}
|
||||
.replace("{functions_dir_env}", &functions_dir_env)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn build_binaries(
|
||||
binary_name: &str,
|
||||
@@ -1218,41 +1247,7 @@ impl Functions {
|
||||
)
|
||||
})?;
|
||||
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let to_script_path = |p: &str| -> String { p.replace('\\', "/") };
|
||||
let content = match binary_type {
|
||||
BinaryType::Tool(None) => {
|
||||
let root_dir = paths::functions_dir();
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
|
||||
.replace("{tool_path}", &to_script_path(&tool_path))
|
||||
}
|
||||
BinaryType::Tool(Some(agent_name)) => {
|
||||
let root_dir = paths::agent_data_dir(agent_name);
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace("{root_dir}", &to_script_path(&root_dir.to_string_lossy()))
|
||||
.replace("{tool_path}", &to_script_path(&tool_path))
|
||||
}
|
||||
BinaryType::Agent => content_template
|
||||
.replace("{agent_name}", binary_name)
|
||||
.replace(
|
||||
"{config_dir}",
|
||||
&to_script_path(&paths::config_dir().to_string_lossy()),
|
||||
),
|
||||
}
|
||||
.replace(
|
||||
"{prompt_utils_file}",
|
||||
&to_script_path(&paths::bash_prompt_utils_file().to_string_lossy()),
|
||||
);
|
||||
let content = Self::render_shim_template(content_template, binary_name, &binary_type);
|
||||
write_file_atomic(&binary_script_file, &content, None)?;
|
||||
|
||||
info!(
|
||||
@@ -1298,21 +1293,17 @@ impl Functions {
|
||||
_ => bail!("Unsupported language: {}", language.as_ref()),
|
||||
}
|
||||
};
|
||||
let bin_dir = binary_file
|
||||
.parent()
|
||||
.expect("Failed to get parent directory of binary file");
|
||||
let canonical_bin_dir = dunce::canonicalize(bin_dir)?.to_string_lossy().into_owned();
|
||||
let wrapper_binary = dunce::canonicalize(&binary_script_file)?
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// %~dp0 (the .cmd's own directory) keeps the launcher relocatable: no
|
||||
// absolute paths may be baked into it (see render_shim_template).
|
||||
let script_name = format!("run-{binary_name}.{}", language.to_extension());
|
||||
let content = formatdoc!(
|
||||
r#"
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "bin_dir={canonical_bin_dir}"
|
||||
set "bin_dir=%~dp0"
|
||||
|
||||
{run} "{wrapper_binary}" %*"#,
|
||||
{run} "%~dp0{script_name}" %*"#,
|
||||
);
|
||||
|
||||
write_file_atomic(&binary_file, &content, None)?;
|
||||
@@ -1352,37 +1343,7 @@ impl Functions {
|
||||
)
|
||||
})?;
|
||||
let content_template = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
||||
let mut content = match binary_type {
|
||||
BinaryType::Tool(None) => {
|
||||
let root_dir = paths::functions_dir();
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace("{root_dir}", &root_dir.to_string_lossy())
|
||||
.replace("{tool_path}", &tool_path)
|
||||
}
|
||||
BinaryType::Tool(Some(agent_name)) => {
|
||||
let root_dir = paths::agent_data_dir(agent_name);
|
||||
let tool_path = format!(
|
||||
"{}/{binary_name}",
|
||||
paths::global_tools_dir().to_string_lossy()
|
||||
);
|
||||
content_template
|
||||
.replace("{function_name}", binary_name)
|
||||
.replace("{root_dir}", &root_dir.to_string_lossy())
|
||||
.replace("{tool_path}", &tool_path)
|
||||
}
|
||||
BinaryType::Agent => content_template
|
||||
.replace("{agent_name}", binary_name)
|
||||
.replace("{config_dir}", &paths::config_dir().to_string_lossy()),
|
||||
}
|
||||
.replace(
|
||||
"{prompt_utils_file}",
|
||||
&paths::bash_prompt_utils_file().to_string_lossy(),
|
||||
);
|
||||
let mut content = Self::render_shim_template(content_template, binary_name, &binary_type);
|
||||
|
||||
if let Some(rt) = custom_runtime
|
||||
&& let Some(newline_pos) = content.find('\n')
|
||||
@@ -1398,9 +1359,10 @@ impl Functions {
|
||||
write_file_atomic(&script_file, &content, Some(0o755))?;
|
||||
|
||||
let ts_runtime = custom_runtime.unwrap_or("tsx");
|
||||
// Locate the script next to the wrapper at run time instead of
|
||||
// baking an absolute path (see render_shim_template).
|
||||
let wrapper = format!(
|
||||
"#!/bin/sh\nexec {ts_runtime} \"{}\" \"$@\"\n",
|
||||
script_file.display()
|
||||
"#!/bin/sh\nexec {ts_runtime} \"$(dirname \"$0\")/run-{binary_name}.ts\" \"$@\"\n",
|
||||
);
|
||||
write_file_atomic(&binary_file, &wrapper, Some(0o755))?;
|
||||
} else {
|
||||
@@ -4613,6 +4575,97 @@ mod tests {
|
||||
fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
#[serial]
|
||||
fn built_shims_resolve_paths_at_runtime_after_relocation() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let root = temp_file("-shim-relocate-", "");
|
||||
let home_a = root.join("home-a");
|
||||
let config_a = home_a.join("coyote");
|
||||
let tools_dir = config_a.join("functions").join("tools");
|
||||
fs::create_dir_all(&tools_dir).unwrap();
|
||||
fs::create_dir_all(config_a.join("functions").join("bin")).unwrap();
|
||||
let tool_src = tools_dir.join("relocheck.sh");
|
||||
fs::write(
|
||||
&tool_src,
|
||||
"#!/usr/bin/env bash\necho \"relocated-ok\" >> \"$LLM_OUTPUT\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::set_permissions(&tool_src, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
let config_env = get_env_name("config_dir");
|
||||
let functions_env = get_env_name("functions_dir");
|
||||
let saved_config = env::var(&config_env).ok();
|
||||
let saved_functions = env::var(&functions_env).ok();
|
||||
unsafe {
|
||||
env::set_var(&config_env, &config_a);
|
||||
env::remove_var(&functions_env);
|
||||
}
|
||||
|
||||
let build_result = Functions::build_global_function_binary("relocheck.sh", None);
|
||||
|
||||
unsafe {
|
||||
match saved_config {
|
||||
Some(v) => env::set_var(&config_env, v),
|
||||
None => env::remove_var(&config_env),
|
||||
}
|
||||
match saved_functions {
|
||||
Some(v) => env::set_var(&functions_env, v),
|
||||
None => env::remove_var(&functions_env),
|
||||
}
|
||||
}
|
||||
build_result.unwrap();
|
||||
|
||||
let shim = config_a.join("functions").join("bin").join("relocheck");
|
||||
let content = fs::read_to_string(&shim).unwrap();
|
||||
assert!(
|
||||
!content.contains(config_a.to_str().unwrap()),
|
||||
"shim must not bake the generation-time config path:\n{content}"
|
||||
);
|
||||
assert!(
|
||||
content.contains(&functions_env),
|
||||
"shim must consult the functions dir env override at run time:\n{content}"
|
||||
);
|
||||
|
||||
// Simulate the config dir landing under a different home directory
|
||||
// (shared/synced config dir, or a sandbox with another $HOME).
|
||||
let home_b = root.join("home-b");
|
||||
fs::rename(&home_a, &home_b).unwrap();
|
||||
let moved_shim = home_b
|
||||
.join("coyote")
|
||||
.join("functions")
|
||||
.join("bin")
|
||||
.join("relocheck");
|
||||
|
||||
if which::which("bash").is_err() || which::which("jq").is_err() {
|
||||
fs::remove_dir_all(&root).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
let output = process::Command::new(&moved_shim)
|
||||
.arg("{}")
|
||||
.env_remove(&config_env)
|
||||
.env_remove(&functions_env)
|
||||
.env_remove("LLM_OUTPUT")
|
||||
.env_remove("LLM_TOOL_DATA_FILE")
|
||||
.output()
|
||||
.unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"relocated shim failed: stderr={} stdout={stdout}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("relocated-ok"),
|
||||
"unexpected output: {stdout}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_tool_calls_partitions_mcp_and_sequential_then_resorts() {
|
||||
let mut ctx = RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd);
|
||||
|
||||
Reference in New Issue
Block a user