mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c099ec9b0 | |||
| 3d3fc04bf7 |
@@ -10,6 +10,7 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.9",
|
||||
"bun-types": "^1.1.23",
|
||||
"dedent": "^1.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^12.0.2",
|
||||
"prettier": "^3.3.3",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
display_name: AgentAPI
|
||||
description: Building block for modules that need to run an agentapi server
|
||||
icon: ../../../../.icons/coder.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [internal]
|
||||
---
|
||||
|
||||
# AgentAPI
|
||||
|
||||
The AgentAPI module is a building block for modules that need to run an agentapi server. It is intended primarily for internal use by Coder to create modules compatible with Tasks.
|
||||
|
||||
We do not recommend using this module directly. Instead, please consider using one of our [Tasks-compatible AI agent modules](https://registry.coder.com/modules?search=tag%3Atasks).
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
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 = "goose-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
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
ARG_PROVIDER='${var.goose_provider}' \
|
||||
ARG_MODEL='${var.goose_model}' \
|
||||
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
|
||||
ARG_INSTALL='${var.install_goose}' \
|
||||
ARG_GOOSE_VERSION='${var.goose_version}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
## For module developers
|
||||
|
||||
For a complete example of how to use this module, see the [goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
|
||||
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
test,
|
||||
afterEach,
|
||||
expect,
|
||||
describe,
|
||||
setDefaultTimeout,
|
||||
beforeAll,
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
expectAgentAPIStarted,
|
||||
} from "./test-util";
|
||||
|
||||
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;
|
||||
moduleVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
const moduleDirName = ".agentapi-module";
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
moduleVariables: {
|
||||
experiment_report_tasks: "true",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
web_app_display_name: "AgentAPI Web",
|
||||
web_app_slug: "agentapi-web",
|
||||
web_app_icon: "/icon/coder.svg",
|
||||
cli_app_display_name: "AgentAPI CLI",
|
||||
cli_app_slug: "agentapi-cli",
|
||||
agentapi_version: "latest",
|
||||
module_dir_name: moduleDirName,
|
||||
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
|
||||
folder: projectDir,
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
projectDir,
|
||||
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||
moduleDir: import.meta.dir,
|
||||
});
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/aiagent",
|
||||
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"),
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
// increase the default timeout to 60 seconds
|
||||
setDefaultTimeout(60 * 1000);
|
||||
|
||||
// we don't run these tests in CI because they take too long and make network
|
||||
// calls. they are dedicated for local development.
|
||||
describe("agentapi", async () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
test("custom-port", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
agentapi_port: "3827",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id, 3827);
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
pre_install_script: `#!/bin/bash\necho "pre-install"`,
|
||||
install_script: `#!/bin/bash\necho "install"`,
|
||||
post_install_script: `#!/bin/bash\necho "post-install"`,
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const preInstallLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/pre_install.log`,
|
||||
);
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/install.log`,
|
||||
);
|
||||
const postInstallLog = await readFileContainer(
|
||||
id,
|
||||
`/home/coder/${moduleDirName}/post_install.log`,
|
||||
);
|
||||
|
||||
expect(preInstallLog).toContain("pre-install");
|
||||
expect(installLog).toContain("install");
|
||||
expect(postInstallLog).toContain("post-install");
|
||||
});
|
||||
|
||||
test("install-agentapi", async () => {
|
||||
const { id } = await setup({ skipAgentAPIMock: true });
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
const respAgentAPI = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"agentapi --version",
|
||||
]);
|
||||
expect(respAgentAPI.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "web_app_order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "web_app_group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "web_app_icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the web app."
|
||||
}
|
||||
|
||||
variable "web_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the web app."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run AgentAPI in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create the CLI workspace app."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app_order" {
|
||||
type = number
|
||||
description = "The order of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "cli_app_group" {
|
||||
type = string
|
||||
description = "The group of the CLI workspace app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "cli_app_icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/claude.svg"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the CLI workspace app."
|
||||
}
|
||||
|
||||
variable "cli_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the CLI workspace app."
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing the agent used by AgentAPI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "install_script" {
|
||||
type = string
|
||||
description = "Script to install the agent used by AgentAPI."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing the agent used by AgentAPI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "start_script" {
|
||||
type = string
|
||||
description = "Script that starts AgentAPI."
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.2.3"
|
||||
}
|
||||
|
||||
variable "agentapi_port" {
|
||||
type = number
|
||||
description = "The port used by AgentAPI."
|
||||
default = 3284
|
||||
}
|
||||
|
||||
variable "module_dir_name" {
|
||||
type = string
|
||||
description = "Name of the subdirectory in the home directory for module files."
|
||||
}
|
||||
|
||||
|
||||
locals {
|
||||
# we always trim the slash for consistency
|
||||
workdir = trimsuffix(var.folder, "/")
|
||||
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
|
||||
encoded_install_script = var.install_script != null ? base64encode(var.install_script) : ""
|
||||
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
|
||||
agentapi_start_script_b64 = base64encode(var.start_script)
|
||||
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||
main_script = file("${path.module}/scripts/main.sh")
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Install and start AgentAPI"
|
||||
icon = var.web_app_icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
|
||||
chmod +x /tmp/main.sh
|
||||
|
||||
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
|
||||
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
|
||||
ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \
|
||||
ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \
|
||||
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
|
||||
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
|
||||
ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \
|
||||
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
|
||||
ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \
|
||||
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
||||
/tmp/main.sh
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "agentapi_web" {
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:${var.agentapi_port}/"
|
||||
icon = var.web_app_icon
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
subdomain = true
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.agentapi_port}/status"
|
||||
interval = 3
|
||||
threshold = 20
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "agentapi_cli" {
|
||||
count = var.cli_app ? 1 : 0
|
||||
|
||||
slug = var.cli_app_slug
|
||||
display_name = var.cli_app_display_name
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
agentapi attach
|
||||
EOT
|
||||
icon = var.cli_app_icon
|
||||
order = var.cli_app_order
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
port=${1:-3284}
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port $port."
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
set -o nounset
|
||||
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
|
||||
WORKDIR="$ARG_WORKDIR"
|
||||
PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT"
|
||||
INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT"
|
||||
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI"
|
||||
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION"
|
||||
START_SCRIPT="$ARG_START_SCRIPT"
|
||||
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
|
||||
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
|
||||
AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
|
||||
set +o nounset
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
if [ ! -d "${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
mkdir -p "${WORKDIR}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
if [ -n "${PRE_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo -n "${PRE_INSTALL_SCRIPT}" >"$module_path/pre_install.sh"
|
||||
chmod +x "$module_path/pre_install.sh"
|
||||
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
|
||||
fi
|
||||
|
||||
echo "Running install script..."
|
||||
echo -n "${INSTALL_SCRIPT}" >"$module_path/install.sh"
|
||||
chmod +x "$module_path/install.sh"
|
||||
"$module_path/install.sh" 2>&1 | tee "$module_path/install.log"
|
||||
|
||||
# Install AgentAPI if enabled
|
||||
if [ "${INSTALL_AGENTAPI}" = "true" ]; then
|
||||
echo "Installing AgentAPI..."
|
||||
arch=$(uname -m)
|
||||
if [ "$arch" = "x86_64" ]; then
|
||||
binary_name="agentapi-linux-amd64"
|
||||
elif [ "$arch" = "aarch64" ]; then
|
||||
binary_name="agentapi-linux-arm64"
|
||||
else
|
||||
echo "Error: Unsupported architecture: $arch"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||
# for the latest release the download URL pattern is different than for tagged releases
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||
else
|
||||
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||
fi
|
||||
curl \
|
||||
--retry 5 \
|
||||
--retry-delay 5 \
|
||||
--fail \
|
||||
--retry-all-errors \
|
||||
-L \
|
||||
-C - \
|
||||
-o agentapi \
|
||||
"$download_url"
|
||||
chmod +x agentapi
|
||||
sudo mv agentapi /usr/local/bin/agentapi
|
||||
fi
|
||||
if ! command_exists agentapi; then
|
||||
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "${START_SCRIPT}" >"$module_path/scripts/agentapi-start.sh"
|
||||
echo -n "${WAIT_FOR_START_SCRIPT}" >"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
|
||||
if [ -n "${POST_INSTALL_SCRIPT}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo -n "${POST_INSTALL_SCRIPT}" >"$module_path/post_install.sh"
|
||||
chmod +x "$module_path/post_install.sh"
|
||||
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
cd "${WORKDIR}"
|
||||
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
import { expect } from "bun:test";
|
||||
|
||||
export const setupContainer = async ({
|
||||
moduleDir,
|
||||
image,
|
||||
vars,
|
||||
}: {
|
||||
moduleDir: string;
|
||||
image?: string;
|
||||
vars?: Record<string, string>;
|
||||
}) => {
|
||||
const state = await runTerraformApply(moduleDir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return { id, coderScript, cleanup: () => removeContainer(id) };
|
||||
};
|
||||
|
||||
export const loadTestFile = async (
|
||||
moduleDir: string,
|
||||
...relativePath: [string, ...string[]]
|
||||
) => {
|
||||
return await Bun.file(
|
||||
path.join(moduleDir, "testdata", ...relativePath),
|
||||
).text();
|
||||
};
|
||||
|
||||
export const writeExecutable = async ({
|
||||
containerId,
|
||||
filePath,
|
||||
content,
|
||||
}: {
|
||||
containerId: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
}) => {
|
||||
await writeFileContainer(containerId, filePath, content, {
|
||||
user: "root",
|
||||
});
|
||||
await execContainer(
|
||||
containerId,
|
||||
["bash", "-c", `chmod 755 ${filePath}`],
|
||||
["--user", "root"],
|
||||
);
|
||||
};
|
||||
|
||||
interface SetupProps {
|
||||
skipAgentAPIMock?: boolean;
|
||||
moduleDir: string;
|
||||
moduleVariables: Record<string, string>;
|
||||
projectDir?: string;
|
||||
registerCleanup: (cleanup: () => Promise<void>) => void;
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||
const { id, coderScript, cleanup } = await setupContainer({
|
||||
moduleDir: props.moduleDir,
|
||||
vars: props.moduleVariables,
|
||||
});
|
||||
props.registerCleanup(cleanup);
|
||||
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||
if (!props?.skipAgentAPIMock) {
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/agentapi",
|
||||
content:
|
||||
props.agentapiMockScript ??
|
||||
(await loadTestFile(import.meta.dir, "agentapi-mock.js")),
|
||||
});
|
||||
}
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
};
|
||||
|
||||
export const expectAgentAPIStarted = async (
|
||||
id: string,
|
||||
port: number = 3284,
|
||||
) => {
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`curl -fs -o /dev/null "http://localhost:${port}/status"`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log("agentapi not started");
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
export const execModuleScript = async (
|
||||
id: string,
|
||||
env?: Record<string, string>,
|
||||
) => {
|
||||
const envArgs = Object.entries(env ?? {})
|
||||
.map(([key, value]) => ["--env", `${key}=${value}`])
|
||||
.flat();
|
||||
const resp = await execContainer(
|
||||
id,
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
],
|
||||
envArgs,
|
||||
);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
return resp;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const args = process.argv.slice(2);
|
||||
const portIdx = args.findIndex((arg) => arg === "--port") + 1;
|
||||
const port = portIdx ? args[portIdx] : 3284;
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
response.writeHead(200);
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
status: "stable",
|
||||
}),
|
||||
);
|
||||
})
|
||||
.listen(port);
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
use_prompt=${1:-false}
|
||||
port=${2:-3284}
|
||||
|
||||
module_path="$HOME/.agentapi-module"
|
||||
log_file_path="$module_path/agentapi.log"
|
||||
|
||||
echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log
|
||||
echo "using port: $port" >>/home/coder/test-agentapi-start.log
|
||||
|
||||
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
|
||||
bash -c aiagent \
|
||||
>"$log_file_path" 2>&1
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const main = async () => {
|
||||
console.log("mocking an ai agent");
|
||||
// sleep for 30 minutes
|
||||
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,67 +54,42 @@ variable "goose_version" {
|
||||
default = "stable"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Goose in the background."
|
||||
default = false
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
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" {
|
||||
variable "goose_provider" {
|
||||
type = string
|
||||
description = "The provider to use for Goose (e.g., anthropic)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_goose_model" {
|
||||
variable "goose_model" {
|
||||
type = string
|
||||
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
|
||||
description = "Custom script to run before installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
variable "additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "goose"
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
@@ -125,7 +100,8 @@ 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_APP_STATUS_SLUG: ${local.app_slug}
|
||||
CODER_MCP_AI_AGENTAPI_URL: http://localhost:3284
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
@@ -139,204 +115,48 @@ EOT
|
||||
|
||||
# Add two spaces to each line of extensions to match YAML structure
|
||||
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 ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
additional_extensions = var.additional_extensions != null ? "\n ${replace(trimspace(var.additional_extensions), "\n", "\n ")}" : ""
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".goose-module"
|
||||
}
|
||||
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Goose"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
module "agentapi" {
|
||||
# TODO: how to reference the agentapi module? We need version pinning.
|
||||
# while the PR is in review, you can reference the module like this:
|
||||
# source = "git::https://github.com/coder/registry.git//registry/coder/modules/agentapi?ref=hugodutka/agentapi-module"
|
||||
source = "../agentapi"
|
||||
|
||||
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
|
||||
set -e
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# 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
|
||||
ARG_PROVIDER='${var.goose_provider}' \
|
||||
ARG_MODEL='${var.goose_model}' \
|
||||
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
|
||||
ARG_INSTALL='${var.install_goose}' \
|
||||
ARG_GOOSE_VERSION='${var.goose_version}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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[@]}")"
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - goose-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -324,3 +324,13 @@ export const writeFileContainer = async (
|
||||
}
|
||||
expect(proc.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
export const readFileContainer = async (id: string, path: string) => {
|
||||
const proc = await execContainer(id, ["cat", path], ["--user", "root"]);
|
||||
if (proc.exitCode !== 0) {
|
||||
console.log(proc.stderr);
|
||||
console.log(proc.stdout);
|
||||
}
|
||||
expect(proc.exitCode).toBe(0);
|
||||
return proc.stdout;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user