goose module

This commit is contained in:
Hugo Dutka 2025-07-03 14:16:15 +02:00
parent 3d3fc04bf7
commit 7c099ec9b0
8 changed files with 422 additions and 228 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -10,6 +10,7 @@
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.9", "@types/bun": "^1.2.9",
"bun-types": "^1.1.23", "bun-types": "^1.1.23",
"dedent": "^1.6.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^12.0.2", "marked": "^12.0.2",
"prettier": "^3.3.3", "prettier": "^3.3.3",

View File

@ -0,0 +1,254 @@
import {
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../agentapi/test-util";
import dedent from "dedent";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
// Cleanup logic depends on the fact that bun's built-in test runner
// runs tests sequentially.
// https://bun.sh/docs/test/discovery#execution-order
// Weird things would happen if tried to run tests in parallel.
// One test could clean up resources that another test was still using.
afterEach(async () => {
// reverse the cleanup functions so that they are run in the correct order
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
interface SetupProps {
skipAgentAPIMock?: boolean;
skipGooseMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_goose: props?.skipGooseMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
goose_provider: "test-provider",
goose_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
if (!props?.skipGooseMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/goose",
content: await loadTestFile(import.meta.dir, "goose-mock.sh"),
});
}
return { id };
};
// increase the default timeout to 60 seconds
setDefaultTimeout(60 * 1000);
describe("goose", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-version", async () => {
const { id } = await setup({
skipGooseMock: true,
moduleVariables: {
install_goose: "true",
goose_version: "v1.0.24",
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`"$HOME/.local/bin/goose" --version`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("1.0.24");
});
test("install-stable", async () => {
const { id } = await setup({
skipGooseMock: true,
moduleVariables: {
install_goose: "true",
goose_version: "stable",
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
`"$HOME/.local/bin/goose" --version`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
});
test("config", async () => {
const expected =
dedent`
GOOSE_PROVIDER: anthropic
GOOSE_MODEL: claude-3-5-sonnet-latest
extensions:
coder:
args:
- exp
- mcp
- server
cmd: coder
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
enabled: true
envs:
CODER_MCP_APP_STATUS_SLUG: goose
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
name: Coder
timeout: 3000
type: stdio
developer:
display_name: Developer
enabled: true
name: developer
timeout: 300
type: builtin
custom-stuff:
enabled: true
name: custom-stuff
timeout: 300
type: builtin
`.trim() + "\n";
const { id } = await setup({
moduleVariables: {
goose_provider: "anthropic",
goose_model: "claude-3-5-sonnet-latest",
additional_extensions: dedent`
custom-stuff:
enabled: true
name: custom-stuff
timeout: 300
type: builtin
`.trim(),
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.config/goose/config.yaml",
);
expect(resp).toEqual(expected);
});
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'pre-install-script'",
post_install_script: "#!/bin/bash\necho 'post-install-script'",
},
});
await execModuleScript(id);
const preInstallLog = await readFileContainer(
id,
"/home/coder/.goose-module/pre_install.log",
);
expect(preInstallLog).toContain("pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.goose-module/post_install.log",
);
expect(postInstallLog).toContain("post-install-script");
});
const promptFile = "/home/coder/.goose-module/prompt.txt";
const agentapiStartLog = "/home/coder/.goose-module/agentapi-start.log";
test("start-with-prompt", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
});
await execModuleScript(id, {
GOOSE_TASK_PROMPT: "custom-test-prompt",
});
const prompt = await readFileContainer(id, promptFile);
expect(prompt).toContain("custom-test-prompt");
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain(
"'goose run --interactive --instructions /home/coder/.goose-module/prompt.txt '",
);
});
test("start-without-prompt", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
});
await execModuleScript(id);
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain("'goose '");
const prompt = await execContainer(id, ["ls", "-l", promptFile]);
expect(prompt.exitCode).not.toBe(0);
expect(prompt.stderr).toContain("No such file or directory");
});
});

View File

@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 2.5" version = ">= 2.7"
} }
} }
} }
@ -54,67 +54,42 @@ variable "goose_version" {
default = "stable" default = "stable"
} }
variable "experiment_use_screen" { variable "install_agentapi" {
type = bool type = bool
description = "Whether to use screen for running Goose in the background." description = "Whether to install AgentAPI."
default = false default = true
} }
variable "experiment_use_tmux" { variable "goose_provider" {
type = bool
description = "Whether to use tmux instead of screen for running Goose in the background."
default = false
}
variable "session_name" {
type = string
description = "Name for the persistent session (screen or tmux)"
default = "goose"
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_auto_configure" {
type = bool
description = "Whether to automatically configure Goose."
default = false
}
variable "experiment_goose_provider" {
type = string type = string
description = "The provider to use for Goose (e.g., anthropic)." description = "The provider to use for Goose (e.g., anthropic)."
default = ""
} }
variable "experiment_goose_model" { variable "goose_model" {
type = string type = string
description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)." description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)."
default = ""
} }
variable "experiment_pre_install_script" { variable "pre_install_script" {
type = string type = string
description = "Custom script to run before installing Goose." description = "Custom script to run before installing Goose."
default = null default = null
} }
variable "experiment_post_install_script" { variable "post_install_script" {
type = string type = string
description = "Custom script to run after installing Goose." description = "Custom script to run after installing Goose."
default = null default = null
} }
variable "experiment_additional_extensions" { variable "additional_extensions" {
type = string type = string
description = "Additional extensions configuration in YAML format to append to the config." description = "Additional extensions configuration in YAML format to append to the config."
default = null default = null
} }
locals { locals {
app_slug = "goose"
base_extensions = <<-EOT base_extensions = <<-EOT
coder: coder:
args: args:
@ -125,7 +100,8 @@ coder:
description: Report ALL tasks and statuses (in progress, done, failed) you are working on. description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
enabled: true enabled: true
envs: envs:
CODER_MCP_APP_STATUS_SLUG: goose CODER_MCP_APP_STATUS_SLUG: ${local.app_slug}
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
name: Coder name: Coder
timeout: 3000 timeout: 3000
type: stdio type: stdio
@ -139,204 +115,48 @@ EOT
# Add two spaces to each line of extensions to match YAML structure # Add two spaces to each line of extensions to match YAML structure
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}" formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : "" additional_extensions = var.additional_extensions != null ? "\n ${replace(trimspace(var.additional_extensions), "\n", "\n ")}" : ""
combined_extensions = <<-EOT
combined_extensions = <<-EOT
extensions: extensions:
${local.formatted_base}${local.additional_extensions} ${local.formatted_base}${local.additional_extensions}
EOT EOT
install_script = file("${path.module}/scripts/install.sh")
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" start_script = file("${path.module}/scripts/start.sh")
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" module_dir_name = ".goose-module"
} }
# Install and Initialize Goose module "agentapi" {
resource "coder_script" "goose" { # TODO: how to reference the agentapi module? We need version pinning.
agent_id = var.agent_id # while the PR is in review, you can reference the module like this:
display_name = "Goose" # source = "git::https://github.com/coder/registry.git//registry/coder/modules/agentapi?ref=hugodutka/agentapi-module"
icon = var.icon source = "../agentapi"
script = <<-EOT
agent_id = var.agent_id
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = "Goose"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Goose CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
install_script = <<-EOT
#!/bin/bash #!/bin/bash
set -e set -o errexit
set -o pipefail
# Function to check if a command exists echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
command_exists() { chmod +x /tmp/install.sh
command -v "$1" >/dev/null 2>&1
}
# Run pre-install script if provided ARG_PROVIDER='${var.goose_provider}' \
if [ -n "${local.encoded_pre_install_script}" ]; then ARG_MODEL='${var.goose_model}' \
echo "Running pre-install script..." ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh ARG_INSTALL='${var.install_goose}' \
chmod +x /tmp/pre_install.sh ARG_GOOSE_VERSION='${var.goose_version}' \
/tmp/pre_install.sh /tmp/install.sh
fi EOT
# Install Goose if enabled
if [ "${var.install_goose}" = "true" ]; then
if ! command_exists npm; then
echo "Error: npm is not installed. Please install Node.js and npm first."
exit 1
fi
echo "Installing Goose..."
RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
fi
# Run post-install script if provided
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
# Configure Goose if auto-configure is enabled
if [ "${var.experiment_auto_configure}" = "true" ]; then
echo "Configuring Goose..."
mkdir -p "$HOME/.config/goose"
cat > "$HOME/.config/goose/config.yaml" << EOL
GOOSE_PROVIDER: ${var.experiment_goose_provider}
GOOSE_MODEL: ${var.experiment_goose_model}
${trimspace(local.combined_extensions)}
EOL
fi
# Write system prompt to config
mkdir -p "$HOME/.config/goose"
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
# Handle terminal multiplexer selection (tmux or screen)
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
echo "Please set only one of them to true."
exit 1
fi
# Determine goose command
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Goose in the background with tmux..."
# Check if tmux is installed
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.goose.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Configure tmux for shared sessions
if [ ! -f "$HOME/.tmux.conf" ]; then
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
# Create a new tmux session in detached mode
tmux new-session -d -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
elif [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Goose in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.goose.log"
# Ensure the screenrc exists
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
/bin/bash
"
fi
EOT
run_on_start = true
}
resource "coder_app" "goose" {
slug = "goose"
display_name = "Goose"
agent_id = var.agent_id
command = <<-EOT
#!/bin/bash
set -e
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Determine goose command
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.experiment_use_tmux}" = "true" ]; then
if tmux has-session -t ${var.session_name} 2>/dev/null; then
echo "Attaching to existing Goose tmux session." | tee -a "$HOME/.goose.log"
tmux attach-session -t ${var.session_name}
else
echo "Starting a new Goose tmux session." | tee -a "$HOME/.goose.log"
tmux new-session -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
# Check if session exists first
if ! screen -list | grep -q "${var.session_name}"; then
echo "Error: No existing Goose session found. Please wait for the script to start it."
exit 1
fi
# Only attach to existing session
screen -xRR ${var.session_name}
else
cd ${var.folder}
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
fi
EOT
icon = var.icon
order = var.order
group = var.group
} }

View File

@ -0,0 +1,57 @@
#!/bin/bash
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
set -o nounset
echo "--------------------------------"
echo "provider: $ARG_PROVIDER"
echo "model: $ARG_MODEL"
echo "goose_config: $ARG_GOOSE_CONFIG"
echo "install: $ARG_INSTALL"
echo "goose_version: $ARG_GOOSE_VERSION"
echo "--------------------------------"
set +o nounset
if [ "${ARG_INSTALL}" = "true" ]; then
echo "Installing Goose..."
parsed_version="${ARG_GOOSE_VERSION}"
if [ "${ARG_GOOSE_VERSION}" = "stable" ]; then
parsed_version=""
fi
curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | GOOSE_VERSION="${parsed_version}" CONFIGURE=false bash
echo "Goose installed"
else
echo "Skipping Goose installation"
fi
if [ "${ARG_GOOSE_CONFIG}" != "" ]; then
echo "Configuring Goose..."
mkdir -p "$HOME/.config/goose"
echo "GOOSE_PROVIDER: $ARG_PROVIDER" >"$HOME/.config/goose/config.yaml"
echo "GOOSE_MODEL: $ARG_MODEL" >>"$HOME/.config/goose/config.yaml"
echo "$ARG_GOOSE_CONFIG" >>"$HOME/.config/goose/config.yaml"
else
echo "Skipping Goose configuration"
fi
if [ "${GOOSE_SYSTEM_PROMPT}" != "" ]; then
echo "Setting Goose system prompt..."
mkdir -p "$HOME/.config/goose"
echo "$GOOSE_SYSTEM_PROMPT" >"$HOME/.config/goose/.goosehints"
else
echo "Goose system prompt not set. use the GOOSE_SYSTEM_PROMPT environment variable to set it."
fi
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi

View File

@ -0,0 +1,35 @@
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
# this must be kept up to date with main.tf
MODULE_DIR="$HOME/.goose-module"
mkdir -p "$MODULE_DIR"
if [ ! -z "$GOOSE_TASK_PROMPT" ]; then
echo "Starting with a prompt"
PROMPT="Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT"
PROMPT_FILE="$MODULE_DIR/prompt.txt"
echo -n "$PROMPT" >"$PROMPT_FILE"
GOOSE_ARGS=(run --interactive --instructions "$PROMPT_FILE")
else
echo "Starting without a prompt"
GOOSE_ARGS=()
fi
agentapi server --term-width 67 --term-height 1190 -- \
bash -c "$(printf '%q ' "$GOOSE_CMD" "${GOOSE_ARGS[@]}")"

View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
const http = require("http");
const args = process.argv.slice(2);
console.log(args);
const port = 3284;
console.log(`starting server on port ${port}`);
http
.createServer(function (_request, response) {
response.writeHead(200);
response.end(
JSON.stringify({
status: "stable",
}),
);
})
.listen(port);

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
while true; do
echo "$(date) - goose-mock"
sleep 15
done