Compare commits

..

77 Commits

Author SHA1 Message Date
Yevhenii Shcherbina 7e42a145fa feat: dropping perms before running claude (#509)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-10-24 15:35:20 -05:00
Atif Ali 0ff3dbcc48 chore(claude-code): limit MCP tools for task reporting (#507) 2025-10-24 23:14:34 +05:00
netsgnut a327e79bc4 fix(kasmvnc): change installed check and bump default version (#505)
## Description

This PR makes the following changes to the `coder/modules/kasmvnc`:
- Change the installation check from checking `vncserver` to
`kasmvncserver`.
- Bump the default KasmVNC installation version to
[1.4.0](https://docs.kasmvnc.com/docs/release_notes/1.4.0).

In images where there is already TightVNC installed, the current
installation check will erroneously report that KasmVNC is already
installed. By checking `kasmvncserver` instead, it ensures KasmVNC is
installed.

Tested on Debian, Kali and Alpine-based images.

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/kasmvnc`  
**New version:** `v1.2.5`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

None
2025-10-24 17:47:51 +00:00
Harsh Singh Panwar bc39c2ee29 Aider module support agentAPI (#356)
Closes #239

/claim #239

## Description

video :-
https://www.loom.com/share/d1d1d54d48bc45c4a48271ca9a387a88?sid=933e250d-78f8-4a7f-9745-0e908c0ee4d9
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [x] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/aider`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-24 10:25:40 -05:00
Danielle Maywood e3ff43c0a6 refactor(coder/agentapi): support terraform-provider-coder v2.12.0 (#485)
In terraform-provider-coder v2.12.0 and the
up-coming coder v2.28 release we have removed the
requirement for the "AI Prompt" parameter, and are intending on slightly
re-designing the API of the AI task modules.

Instead of `agentapi` defining the `coder_ai_task` resource, it will
output the `task_app_id`. Consumers of the module will then be expected
to create the `coder_ai_task` resource themselves with this
`task_app_id`.
2025-10-24 11:54:12 +01:00
Yevhenii Shcherbina 30123e7ea3 feat: add boundary pprof server in claude-code module (#503) 2025-10-23 14:18:30 -04:00
djarbz f7c1be71f7 Add [copyparty] module (#486)
## Description

This PR adds a module to install Copyparty as an alternative to
Filebrowser.

## Type of Change

- [x] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/djarbz/modules/copyparty`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [N/A] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
None

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-23 11:19:05 -05:00
DevCats 19519a0a13 fix: add shebang to zed coder_script (#504)
## Description

Add `#!/bin/sh` to zed_settings coder_script
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/zed`  
**New version:** `v1.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

https://github.com/coder/registry/issues/482
<!-- Link related issues or write "None" if not applicable -->
2025-10-23 07:39:27 -05:00
Rowan Smith 63e42283ce chore: Update templates from Always to IfNotPresent for image_pull_policy (#501)
## Description

Change `image_pull_policy` from `Always` to `IfNotPresent` on Coder
owned templates. Given these are a reference point for users and
customers and they copy them into their own templates I think it makes
sense to encourage the use of caching of images.

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Template Information

**Path:** 


https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-devcontainer

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-envbox

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

None
2025-10-23 04:28:58 +00:00
Eric Paulsen 0c5a8a2354 add nfs-deployment template (#502)
## Description

this PR adds a new template to the registry, which shows how to mount an
NFS share to a K8s deployment workspace.

## Type of Change

- [ ] New module
- [x] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Template Information

<!-- Delete this section if not applicable -->

**Path:** `registry/ericpaulsen/templates/nfs-deployment`

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

None

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-22 11:51:58 -05:00
DevCats 51ec6e3212 fix: resolve issues with claude-code session resumption (#496)
## Description

Fixes session resumption logic by having the continue flag decide
whether to continue a workspace based on session history

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [X] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.2.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 10:58:01 -05:00
DevCats 843b1f1e5a chore: change copilot default version to latest (#499)
## Description

Changes `copilot_version` default to `latest`

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.2.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-22 07:33:09 -05:00
Jiachen Jiang 583918bfef added example of boundary to claude code module (#500) 2025-10-21 16:33:15 -05:00
Benjamin Peinhardt a1786a09ea update claude-code module version (#498)
The version for the claude-code module should have been updated in
https://github.com/coder/registry/pull/455. This PR updates the module
version so we can cut a release 😎
2025-10-21 13:46:32 -05:00
Benjamin Peinhardt a35986d7df feat: initial boundary integration with claude code (#455)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: YEVHENII SHCHERBINA <yevhenii@coder.com>
2025-10-21 13:44:26 -04:00
Mathias Fredriksson e34320cb0b feat: add archive module (#422)
This change adds a new `archive` module to the Coder registry. It can be
used to archive user-data from pre-defined locations and restore it as
well.

Here we also explore:

- A new method of passing arrays from Terraform to Bash
- A new method of writing Bash scripts that minimizes the interaction
with terraform interpolation
- Extensive test-suite that not only tests that Terraform options can be
selected, but also the resulting script behaviors

---------

Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-17 08:14:56 -05:00
35C4n0r ca7bc42946 feat: update auth setup in codex (#472)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [x] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/codex`  
**New version:** `v3.0.0`  
**Breaking change:** [X] Yes [ ] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-16 15:25:57 -05:00
35C4n0r a599302774 feat: amp upgrades for better ux (#390)
Closes #

## Description
- remove default node installation
- users can pass amp versions now
- move env variables to terraform variable (system prompt and ai prompt)

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/sourcegraph-amp`  
**New version:** `v2.0.0`  
**Breaking change:** [x] Yes [ ] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <me@matifali.dev>
2025-10-16 15:21:17 -05:00
DevCats ff09c415e8 feat: change tf test and validation to use paths-filter (#483)
## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Template Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/templates/[template-name]`

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-16 14:21:03 -05:00
DevCats 90873e8009 ci: update CI workflow to run TypeScript tests with new script (#480) 2025-10-15 14:03:12 -05:00
DevCats 2168360195 fix: add folder to all Agent Modules (#481)
## Description

Make sure folder is passed to agentapi in all Agent modules.
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder-labs/modules/auggie`  
**New version:** `v0.2.1`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder-labs/modules/cursor-cli`  
**New version:** `v0.2.1`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder-labs/modules/gemini`  
**New version:** `v2.1.1`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder-labs/modules/sourcegraph_amp`  
**New version:** `v1.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-15 12:20:08 -05:00
Riajul Islam da5a2ba6a8 feat(git-clone module): added post_clone_script. (#357)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-10-15 12:53:17 +00:00
Anas 63cad25954 fix(amazon-q): pass workdir variable into agentapi folder variable (#478)
Co-authored-by: DevCats <christofer@coder.com>
2025-10-15 17:44:58 +05:00
Hulto cd759bd9a1 goose module: pass folder along to agentapi (#412)
Co-authored-by: DevCats <christofer@coder.com>
2025-10-15 17:44:18 +05:00
DevCats 54a7bb0001 docs: add usage examples for bedrock and vertex (#431)
Closes #

## Description

Adds Usage Examples for Vertex and Bedrock as described in the linked
documentation.

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-14 12:06:32 -05:00
Matt Hazinski 50f4d5388b fix(codex): pass folder variable to agentapi module (#477)
## Description

The folder variable was not being passed from the codex module to the
agentapi module, causing agentapi to use its default value of
`/home/coder` instead of the user-specified folder path.

This resulted in permission errors when the codex module tried to create
directories in `/home/coder` when users specified a different folder
like `/home/matt/foo`.

Fix by adding `folder = var.folder` to the agentapi module invocation.

## Type of Change

- [ ] New module
- [ ] New template
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

Path: registry/coder-labs/modules/codex
New version: v2.1.1
Breaking change: [ ] Yes [X] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues
Fixes https://github.com/coder/registry/issues/476

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-10-14 16:49:52 +00:00
dependabot[bot] 36943d1dfb chore(deps): bump crate-ci/typos from 1.37.2 to 1.38.1 in the github-actions group (#475)
Bumps the github-actions group with 1 update:
[crate-ci/typos](https://github.com/crate-ci/typos).

Updates `crate-ci/typos` from 1.37.2 to 1.38.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/releases">crate-ci/typos's
releases</a>.</em></p>
<blockquote>
<h2>v1.38.1</h2>
<h2>[1.38.1] - 2025-10-07</h2>
<h3>Fixes</h3>
<ul>
<li>Ignore common golang identifiers</li>
</ul>
<h2>v1.38.0</h2>
<h2>[1.38.0] - 2025-10-06</h2>
<h3>Features</h3>
<ul>
<li>Update type list</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>typ</code></li>
<li>Consistently error on unused config fields</li>
</ul>
<h2>v1.37.3</h2>
<h2>[1.37.3] - 2025-10-06</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>PN</code> for <code>bitbake</code> file
types</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's
changelog</a>.</em></p>
<blockquote>
<h2>[1.38.1] - 2025-10-07</h2>
<h3>Fixes</h3>
<ul>
<li>Ignore common golang identifiers</li>
</ul>
<h2>[1.38.0] - 2025-10-06</h2>
<h3>Features</h3>
<ul>
<li>Update type list</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>typ</code></li>
<li>Consistently error on unused config fields</li>
</ul>
<h2>[1.37.3] - 2025-10-06</h2>
<h3>Fixes</h3>
<ul>
<li>Don't correct <code>PN</code> for <code>bitbake</code> file
types</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/crate-ci/typos/commit/80c8a4945eec0f6d464eaf9e65ed98ef085283d1"><code>80c8a49</code></a>
chore: Release</li>
<li><a
href="https://github.com/crate-ci/typos/commit/c1008ce1b695c69fa611c3a79c32852be029709a"><code>c1008ce</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/crate-ci/typos/commit/62a3b5083afa59e8054f76ff5dbb94bb676ce5e4"><code>62a3b50</code></a>
Merge pull request <a
href="https://redirect.github.com/crate-ci/typos/issues/1398">#1398</a>
from ccoveille-forks/go-exclusions</li>
<li><a
href="https://github.com/crate-ci/typos/commit/e6bedbde77058052de3f00d82a67284618385615"><code>e6bedbd</code></a>
fix(config): Add some Go exclusions</li>
<li><a
href="https://github.com/crate-ci/typos/commit/90cacd60e824aaf9adff4afa0d6582f52631bc6d"><code>90cacd6</code></a>
docs(ref): Speak to glob ambiguity</li>
<li><a
href="https://github.com/crate-ci/typos/commit/b81b12ea1b8702b57e1a917e5a7bfc26d46c21e9"><code>b81b12e</code></a>
docs(ref): Clarify directories are not spell checked</li>
<li><a
href="https://github.com/crate-ci/typos/commit/eaf25df9941e5b6a2f145729a76b06af9eab44ca"><code>eaf25df</code></a>
docs(ref): Speak to locale's behavior</li>
<li><a
href="https://github.com/crate-ci/typos/commit/a9735e2e141b9a8f08340e41aac57b52805ae185"><code>a9735e2</code></a>
docs(ref): Provide identifier/word config examples</li>
<li><a
href="https://github.com/crate-ci/typos/commit/3c14191fcc71bad3e87e231c86a6d3a7876ae8a2"><code>3c14191</code></a>
docs(ref): Talk about include lists</li>
<li><a
href="https://github.com/crate-ci/typos/commit/d0f81dc972d4fc9862590daafb26ee03a2dbfda5"><code>d0f81dc</code></a>
docs(ref): Re-organize help more like cargo</li>
<li>Additional commits viewable in <a
href="https://github.com/crate-ci/typos/compare/v1.37.2...v1.38.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.37.2&new-version=1.38.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 07:40:01 -05:00
greg-the-coder e7d705bf98 Fixes from AWS Workshop testing (#428)
Closes #

## Description

Changes to code-server and jetbrains modules that were not caught during
initial unit-testing, that appear to be related to older versions of the
modules or recent changes.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

None

---------

Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-10-10 06:27:18 +05:00
Atif Ali 898219b16b Enhance PR template with template information section (#474) 2025-10-09 15:50:38 +00:00
chgl fc071e0930 refactor: refactored get_http_dir (#360)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

I just couldn't get the script to execute properly in its current form.
I saw e.g.

```console
[[: 1989{#d[@]}: syntax error: invalid arithmetic operator (error token is "{#d[@]}")
```

when trying to run the script locally. (GNU bash, version
5.2.21(1)-release (x86_64-pc-linux-gnu)).

This uses a likely simpler bash script, but requires both grep and awk.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/kasmvnc`  
**New version:** `v1.2.3`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-09 09:36:14 -05:00
DevCats d516aff908 chore: set verified to false and bump to 1.0.1 (#473)
## Description

Removes verified status from nexus module.
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [X] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/mavrickrishi/modules/nexus-repository`  
**New version:** `v1.0.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-09 08:13:06 -05:00
DevCats ccdca6daf5 chore: update CONTRIBUTION docs to explain both tests, and update CI for both tests (#384)
Closes #383 

## Description

- Update CONTRIBUTION.md to elaborate on ts and tf tests
- Add ./scripts/terraform_test_all.sh to CI for ts tests

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [X] Other

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-10-09 12:42:07 +00:00
Rishi Mondal ce039f64df Add Sonatype Nexus repository integration module (#262)
# Add Sonatype Nexus Repository Integration Module

## Summary
Implements a Coder module for Sonatype Nexus Repository Manager
integration that automatically configures Maven, npm, PyPI, and Docker
registries for development workspaces.

## Demo Video & Screenshots

https://github.com/user-attachments/assets/2c51f229-d34d-483b-a0e9-f4e0d79332c2

![Nexus Repository
Integration](https://github.com/user-attachments/assets/1a778a8f-0e48-40f2-ae0f-5b8d5d5ce849)

## Features
-  **Maven Support**: Automatic `settings.xml` configuration
-  **npm Support**: Automatic `.npmrc` configuration with scoped
packages
-  **PyPI Support**: Automatic `pip.conf` configuration
-  **Docker Support**: Registry authentication setup
-  **Flexible Configuration**: Support for multiple repositories per
package manager
-  **Secure Credentials**: API token and password support
-  **Username Options**: Configurable username field (username or
email)

## Nexus Repository Manager Requirements

### Version Requirements
**Yes, this module requires Nexus Repository Manager Pro version** for
full functionality, though basic features work with the Community
Edition (OSS).

### Supported Authentication Methods
This module supports **4 authentication methods**:

1. **User Token Authentication** (Recommended - Pro only)
   - Enhanced security with two-part tokens
   - Ideal for CI/CD and automated environments
   - Requires `nx-usertoken-current` privilege

2. **API Token Authentication** (Pro only)
   - Single-use access tokens via REST API
   - Programmatic token generation and management

3. **Basic Authentication** (OSS & Pro)
   - Standard HTTP Basic Auth with username/password
   - Works with both OSS and Pro versions

4. **Base64 Encoded Credentials** (OSS & Pro)  
   - Base64 encoded `username:password` format
   - Compatible with npm and other package managers

### Testing Instructions

#### Prerequisites
- Nexus Repository Manager instance (OSS or Pro)
- Admin access to configure repositories
- Test repositories for each package manager you want to test

#### Setup Test Environment
1. **Create Test Repositories** in your Nexus instance:
   - Maven: `maven-public`, `maven-releases` 
   - npm: `npm-public`, `@company:npm-private`
   - PyPI: `pypi-public`, `pypi-private`
   - Docker: `docker-public`, `docker-private`

2. **Configure Authentication**:
   - For Pro: Generate user tokens via UI (User menu → User Token)
   - For OSS: Use username/password or base64 encoded credentials
   - Set up appropriate permissions for test repositories

3. **Test the Module**:
   ```hcl
   module "nexus" {
     source         = "registry.coder.com/mavrickrishi/nexus/coder"
     version        = "1.0.0"
     agent_id       = coder_agent.main.id
     nexus_url      = "https://your-nexus-instance.com"
     nexus_password = var.nexus_api_token  # or password
     package_managers = {
       maven  = ["maven-public", "maven-releases"]
       npm    = ["npm-public", "@company:npm-private"]
       pypi   = ["pypi-public", "pypi-private"]
       docker = ["docker-public", "docker-private"]
     }
   }
   ```

4. **Verify Configuration**:
   - Check generated config files in workspace
   - Test package installation from configured repositories
   - Verify authentication works for each package manager

#### EC2 Deployment Testing
Tested by deploying on EC2 instance with:
- Ubuntu 22.04 LTS
- Nexus Repository Manager Pro
- All package managers (Maven, npm, PyPI, Docker)
- Both token and basic authentication methods

## Usage Example
```hcl
module "nexus" {
  source         = "registry.coder.com/mavrickrishi/nexus/coder"
  version        = "1.0.0"
  agent_id       = coder_agent.main.id
  nexus_url      = "https://nexus.company.com"
  nexus_password = var.nexus_api_token
  package_managers = {
    maven  = ["maven-public", "maven-releases"]
    npm    = ["npm-public", "@company:npm-private"]
    pypi   = ["pypi-public", "pypi-private"]
    docker = ["docker-public", "docker-private"]
  }
}
```

## Testing
-  11 comprehensive tests covering all functionality
-  Variable validation tests
-  Package manager configuration tests
-  Error handling tests
-  All tests passing
-  EC2 deployment tested

## Files Added
- `registry/mavrickrishi/modules/nexus/main.tf` - Main module
configuration
- `registry/mavrickrishi/modules/nexus/README.md` - Complete
documentation
- `registry/mavrickrishi/modules/nexus/main.test.ts` - Test suite

## Checklist
- [x] Module follows existing patterns and conventions
- [x] Comprehensive test coverage (11 tests)
- [x] Complete documentation with examples
- [x] Input validation and error handling
- [x] Secure credential handling
- [x] All tests passing
- [x] Demo video included
- [x] Screenshots added
- [x] Testing instructions provided
- [x] Authentication methods documented
- [x] EC2 deployment tested

Closes #202
/claim #202

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Atif Ali <me@matifali.dev>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
2025-10-09 07:31:43 -05:00
DevCats 8acda84dd7 chore: update icons for auto-start-dev-server module (#471)
## Description

Adds icons for module, and update all refrences.

PR for Site Icon's Addition: https://github.com/coder/coder/pull/20219

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/mavrickrishi/modules/auto-start-dev-server`  
**New version:** `v1.0.1`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-08 13:20:45 -05:00
Atif Ali 76c1299968 docs: upgrade alert style to a GFM style tip for JetBrains Gateway (#468) 2025-10-08 06:39:54 +00:00
Jullian Pepito 60372ff797 fix(git-clone): Update README.md (#448)
Changes `coder_git_auth` to `coder_external_auth` in README

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/git-clone` 
**New version:** `v1.1.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Jullian Pepito <jullian@MacBook-Pro.local>
Co-authored-by: DevCats <christofer@coder.com>
2025-10-07 15:35:02 -05:00
Rishi Mondal f28bcdb713 Auto-Start Development Servers Module (#316)
# Auto-Start Development Servers Module

## Summary

/claim #204

Implements automatic detection and startup of development servers based
on project detection as requested in #204.

-  **Multi-language support**: Node.js, Rails, Django, Flask, Spring
Boot, Go, PHP, Rust, .NET
-  **Background execution**: Servers start automatically without user
intervention
-  **Devcontainer.json integration**: Uses custom start commands when
available
-  **Smart fallback**: Creates sample project when no existing projects
found
-  **Comprehensive logging**: Full activity logs for troubleshooting



https://github.com/user-attachments/assets/2eddf67c-3ac1-4e55-a5ba-79292d61e918



## Addresses GitHub Issue

Closes #204 - "Auto-start development servers based on project
detection"

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: DevCats <chris@dualriver.com>
2025-10-07 14:44:00 -05:00
romracer cb553209a5 fix: update CLI icon for copilot module to same icon as web app (#469)
## Description

Sets `cli_app_icon` in agentapi to the same icon used for
`web_app_icon`. Its currently using the default of Claude.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.2.1`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-07 13:54:14 -05:00
35C4n0r 5d0504aef9 feat: update agentapi_version to 0.10.0 (#456)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-07 13:27:12 -05:00
35C4n0r c1c0dec90f chore: bump agentapi module version (#465) 2025-10-07 18:09:44 +00:00
DevCats 59b67c2c98 chore: update display name for copilot module to Copilot CLI (#467)
## Description

update display name for copilot module to Copilot CLI

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 17:40:23 +00:00
DevCats 7abe422e0a fix: Add COPILOT_MODEL to install script args (#464)
Closes #462

## Description

<!-- Briefly describe what this PR does and why -->
Fixes missing COPILOT_MODEL arg from install script

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/copilot`  
**New version:** `v0.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-10-07 12:05:50 -05:00
Susana Ferreira db8217e4e5 fix(claude-code): update inner system prompt to include summary rules (#461)
## Description

Update `report_tasks_system_prompt` to include `coder_report_task`
summary rules.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.3`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

Follow-up from: https://github.com/coder/registry/pull/443
Related to: https://github.com/coder/coder/pull/20191/files#r2410441026
2025-10-07 15:26:09 +01:00
DevCats f75afeb0c8 feat: New Copilot-CLI Module (#441)
## Description

New Copilot-CLI Module using AgentAPI

Need to test once AgentAPI Changes are pushed.

## Type of Change

- [X] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder-labs/modules/copilot-cli`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-10-07 07:47:02 -05:00
Susana Ferreira 182e5548e2 chore: update MAINTAINER.md to check PR version label (#460)
## Description

Update MAINTAINER.md to include a check of the version label on the PRs

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [x] Documentation
- [ ] Other
2025-10-07 10:47:41 +01:00
Susana Ferreira d057a820c1 feat(claude-code): add coder-specific prompt to system_prompt (#443)
## Description

This PR updates the `claude-code` module to automatically include the
Coder task-reporting system prompt whenever `report_tasks = true`, and
to wrap the final system prompt in `<system>…</system>` when non-empty.

Previously, users needed to manually include this content in their
system prompts to enable proper task reporting. When `report_tasks =
true`, the system prompt is prepended with the Coder task-reporting, and
any user `system_prompt` (if provided) is appended after it, ensuring
consistent integration without manual copy/paste.

When `report_tasks = false`, the module includes only the user
`system_prompt` (if any). If both `report_tasks = false` and
`system_prompt` is empty, the system prompt sent to Claude is empty.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v3.0.2` 
**Breaking change:** [] Yes [x]  No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

Related to internal slack thread:
https://codercom.slack.com/archives/C0992H8HGCS/p1759317555713269

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-07 10:09:49 +01:00
Satbir Chahal b4e9545c35 fix(claude-code): source bashrc file only if it exists (#459) 2025-10-07 07:33:17 +00:00
DevCats 50ac3b31f6 docs: add MAINTAINER.md link to CONTRIBUTING.md and README.md (#453)
## Description

<!-- Briefly describe what this PR does and why -->
Add links to `MAINTAINER.md` in `README.md` and `CONTRIBUTING.md` to
help guide internal contributors.

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [X] Documentation
- [ ] Other

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-10-06 12:52:58 -05:00
dependabot[bot] 056937a758 chore(deps): bump crate-ci/typos from 1.36.3 to 1.37.2 in the github-actions group (#451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M Atif Ali <atif@coder.com>
2025-10-06 07:59:23 -05:00
Rowan Smith af8b4f02fd chore: fix for jetbrains gateway agent_id issue (#437)
## Description

Fixes a regression added in #167 which implemented support for multiple
agents by appending the agent id to the URI, however in a single agent
environment it results in the agent id from the template apply (on
upload to Coder from client) being injected, and when a workspace is
later built using the template the agent id is no longer correct.

Resolves the error `The workspace “<name>” does not have an agent with
ID “<id>”` being thrown by Jetbrains Gateway app upon attempting to open
a Jetbrains app from within a Coder workspace.

When wishing to target a specific Coder Agent with the Jetbrains Gateway
module one should use the `agent_name` variable in the module
configuration to specify the desired agent name. This will append the
agent name to the URI.

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/jetbrains-gateway`  
**New version:** `v1.2.4`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

Reported by customer on Zendesk ticket 4391
2025-10-06 08:29:33 +11:00
Susana Ferreira 2de6a57a3f fix: claude-code api_key terraform test (#444)
## Description

Fix claude-code module `test_claude_code_with_api_key` terraform test.
2025-10-01 18:21:54 -05:00
Jiachen Jiang 60fec19d7d Update README.md (#440)
Added recommendation to the Gateway README, pointing to the Toolbox
module.

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-09-30 09:14:16 -07:00
Atif Ali 44354b202d Fix claude-code module not passing workdir to agentapi (#439)
## Summary

Fixes #436 - The claude-code 3.0.0 module was not passing the custom
`workdir` variable to the agentapi module, causing it to default to
`/home/coder` instead of using the specified working directory.

## Changes

- Added missing `folder = local.workdir` parameter to the agentapi
module call in `main.tf:247`
- This ensures that custom working directories are properly propagated
to the agentapi module

## Test Plan

- [x] Terraform validation passes
- [x] Code formatting applied with `bun run fmt`
- [x] Basic terraform test passes (one pre-existing test failure
unrelated to this change)

## Verification

The fix adds the missing parameter that was identified in the issue:
```terraform
module "agentapi" {
  # ... other parameters
  folder = local.workdir  # <- Added this line
  # ... rest of configuration
}
```

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-09-30 08:02:35 -05:00
dependabot[bot] 80acbd7e3a chore(deps): bump crate-ci/typos from 1.36.2 to 1.36.3 in the github-actions group (#438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-30 12:53:03 +00:00
DevCats 80f429faf1 chore: remove it wrappers from required variables tests (#442)
## Description

<!-- Briefly describe what this PR does and why -->
Remove it wrappers from required variables tf test in jfrog-oauth and
jfrog-token modules. This solves the failing tf tests that we were
encountering in all PR's across the board.

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2025-09-30 07:44:41 -05:00
Benraouane Soufiane e516446d03 Add Rustdesk module (#266)
Closes #79

## Description
This PR add new module, install minimal desktop environment (xfce),
virtual display, ,rustdesk package from deb file, init new screen,
export DISPLAY environment variable with last created virtual screen,
start new xfce session & execute the rustdesk cli, generate new
password, change the default password, then log the ID & password to be
used within rustdesk client to connect to the host

## Type of Change

- [x] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information
Overview/test video: live demo that launch rustdesk with GUI in a docker
container https://youtu.be/_rR-l7nARN4
Screenshots: 
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/ba67a864-4295-471e-8b6a-976c23cb8f55"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/24686339-aba7-47fe-92b4-5700ef5b154a"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/21884c31-9eed-45ef-b3de-c12c99f2aa96"
/>
<img width="1920" height="1080" alt="image"
src="https://github.com/user-attachments/assets/ec0c65fe-61be-404c-ba36-8cc2882e85a2"
/>







**Path:** `registry/BenraouaneSoufiane/modules/rustdesk`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

/claim #79 (remain asset 150$)

---------

Co-authored-by: root <root@DESKTOP-6QN3GRE.localdomain>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 20:04:24 -05:00
Rafael Rodriguez f0045397d4 feat: add tooltip support to jetbrains module (#421)
## Description

In this pull request we're updating the JetBrains module to support the
tooltip field added as requested in
https://github.com/coder/coder/pull/19781#pullrequestreview-3214217375

## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/jetbrains`  
**New version:** `v1.1.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

https://github.com/coder/coder/issues/18431

---------

Co-authored-by: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com>
2025-09-22 13:29:12 -05:00
DevCats 6af8508bc0 chore: update tasks template for claude-code update (#423)
## Description

Refactor template for claude-code module update for tasks

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [X] Other

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally

## Related Issues

https://github.com/coder/registry/pull/402

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-09-19 14:51:37 -05:00
35C4n0r d212de47ed feat: refactor claude code to use agentapi module (#402)
Closes #302

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v3.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->

---------

Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <me@matifali.dev>
2025-09-18 10:04:52 -05:00
Marcin Tojek 54b9bf3038 add: nextflow module (#416) 2025-09-18 09:30:48 +00:00
Cian Johnston cb990bbee0 fix(registry/modules/claude-code): default subdomain to false (#419)
Relates to https://github.com/coder/coder/issues/18779

By default, we set `subdomain = true`. Most folks testing this out don't
have a wildcard subdomain setup. This switches to path-based behaviour
by default and adds a note to the troubleshooting section.
2025-09-15 13:09:12 +01:00
Cian Johnston 213aabb3b0 fix(registry/modules/goose): default subdomain to false (#420)
Relates to https://github.com/coder/coder/issues/18779

See also https://github.com/coder/registry/pull/419

By default, we set subdomain = true. Most folks testing this out don't
have a wildcard subdomain setup. This switches to path-based behaviour
by default and adds a note to the troubleshooting section.
2025-09-15 09:00:07 +01:00
david-fraley 2937286712 feat: update Tasks on Docker Template (#418) 2025-09-11 21:53:22 +00:00
Atif Ali 8d556a8ab7 chore: update KasmVNC README note (#417) 2025-09-11 15:46:44 +05:00
Michael Orlov 16015559e2 feat: refactor Amazon Q module to use agentAPI (#362)
### **Title:**
feat: complete amazon-q module v2.0.0 with comprehensive enhancements


### **Description:**
Closes #240

This PR introduces a complete rewrite and enhancement of the amazon-q
module, bringing it to version 2.0.0. The module now provides AgentAPI
support.

## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [x] Documentation
- [ ] Other

## Module Information

**Path:** `registry/coder/modules/amazon-q`
**New version:** `v2.0.0`
**Breaking change:** [x] Yes [ ] No

## Key Features & Enhancements

### 🚀 Core Functionality
- **AgentAPI Support**: Web and CLI app integration with health checks
- **Amazon Q CLI Integration**: Automatic installation and configuration
of Amazon Q CLI
- **MCP Integration**: Model Context Protocol support for task reporting
to Coder
- **Authentication System**: Tarball-based authentication with
environment variable management

### 🛠️ Customization & Configuration
- **Pre/Post Install Scripts**: Support for custom setup and
finalization scripts
- **Agent Configuration**: Templated agent config with tool and resource
management
- **Custom System Prompts**: Configurable AI behavior and task reporting
instructions
- **Version Pinning**: Support for specific Amazon Q CLI and AgentAPI
versions

### 📚 Documentation & Testing
- **Comprehensive README**: Complete user guide with examples,
configuration details, and troubleshooting
- **Visual Documentation**: Updated screenshots and interface examples
- **Terraform Testing**: Complete .tftest.hcl with 8 test cases (all
passing)
- **Registry Compliance**: Full adherence to Coder Registry contributing
guidelines

d## Breaking Changes

This is a major version update (v2.0.0) with breaking changes:
- Renamed variables names (Removed experimantal_ prefix)
- Updated AgentAPI integration method
- Modified default configuration structure

## Testing & Validation

- [x] Tests pass (`terraform test` - 8/8 tests passing)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
- [x] Registry compliance verified
- [x] Documentation reviewed and updated

## Related Issues

Closes #240 - Amazon Q module enhancement request

## Additional Notes

- Module is now production-ready with professional quality code and
documentation
- Full compliance with Coder Registry contributing guidelines
- Comprehensive test coverage ensures reliability
- Ready for registry submission and community use

## Screenshots:
<img width="3001" height="1068" alt="image"
src="https://github.com/user-attachments/assets/24453cb3-d4dc-4a45-bb62-7a834940ebae"
/>
<img width="1209" height="600" alt="image"
src="https://github.com/user-attachments/assets/f2b18c42-ba7f-4e16-a9e7-d51ad1095712"
/>
<img width="1505" height="1251" alt="image"
src="https://github.com/user-attachments/assets/3e6e49b1-808d-482e-a237-b606e50262f5"
/>


https://github.com/user-attachments/assets/6533dead-35f1-47f5-875a-3cebb81453c9



https://github.com/user-attachments/assets/da8047f6-7023-4e6c-af90-138541298089

/claim #240

Co-authored-by: Michael Orlov <michaelo@amdocs.com>
2025-09-10 18:50:22 -05:00
हिमांशु f1010ee7a6 Add maven package manager support to JFrog modules (#414)
Closes #33 
/claim #33 
## Description
Jfrog Modules doesn't support conda package manager, This PR adds
support of that



## Type of Change

- [ ] New module
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

## Related Issues

#33 
## video


https://github.com/user-attachments/assets/61c33963-e1a7-43e2-b1cc-fdb747405cf5
2025-09-09 10:36:31 +05:00
dependabot[bot] 17734c073a chore(deps): bump the github-actions group across 1 directory with 5 updates (#415)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 16:54:57 +05:00
greg-the-coder 6813e0b5b8 Fix/kubernetes devcontainer template to require fewer updates on initial deployment (#386)
Co-authored-by: greg-the-coder <greg@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-09-07 21:48:49 +05:30
Atif Ali 9e47369905 chore: mark AMP CLI agent as verified (#408) 2025-09-03 04:37:22 +00:00
हिमांशु d9d44ca338 fix: bump versions of jfrog-oauth and jfrog-token (#407)
Closes #

## Description

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [x] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

## Related Issues
Follow up of #375
2025-09-01 22:31:43 +05:00
हिमांशु 7152b85246 feat: add conda support to JFrog modules (#375) 2025-09-01 21:54:23 +05:00
Atif Ali 41c6bece3e fix: use correct source url (#404) 2025-09-01 12:56:04 +00:00
Marcin Tojek 9452763f7d add: rstudio module (#327) 2025-09-01 14:25:28 +02:00
m4rrypro 77328656ff feat: add linode vm template (#367)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:07:54 +05:00
m4rrypro c4c484089f feat: add digitalocean region module (#355)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:06:08 +05:00
blink-so[bot] 7e53098bea Update jetbrains-gateway module references to coder/jetbrains (#396)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-29 17:46:25 +05:00
Lucas Kaplan 901043bb01 chore: Fixed path to avatar (#400)
This PR fixes a path issue in the README of the AJ0070 profile. The
original avatar path had the wrong extension.

---------

Co-authored-by: Benjamin <benjaminpeinhardt@gmail.com>
2025-08-28 18:56:11 -04:00
166 changed files with 11751 additions and 2394 deletions
+8 -3
View File
@@ -1,5 +1,3 @@
Closes #
## Description
<!-- Briefly describe what this PR does and why -->
@@ -7,6 +5,7 @@ Closes #
## Type of Change
- [ ] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
@@ -20,10 +19,16 @@ Closes #
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
## Template Information
<!-- Delete this section if not applicable -->
**Path:** `registry/[namespace]/templates/[template-name]`
## Testing & Validation
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Code formatted (`bun fmt`)
- [ ] Changes tested locally
## Related Issues
+3
View File
@@ -1,9 +1,12 @@
[default.extend-words]
muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
inh = "inh" # Option in setpriv command
exportfs = "exportfs" # nfs related binary
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
+37 -3
View File
@@ -13,6 +13,26 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v5
- name: Detect changed files
uses: dorny/paths-filter@v3
id: filter
with:
list-files: shell
filters: |
shared:
- 'test/**'
- 'package.json'
- 'bun.lock'
- 'bunfig.toml'
- 'tsconfig.json'
- '.github/workflows/ci.yaml'
- 'scripts/ts_test_auto.sh'
- 'scripts/terraform_test_all.sh'
- 'scripts/terraform_validate.sh'
modules:
- 'registry/**/modules/**'
all:
- '**'
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
@@ -27,8 +47,22 @@ jobs:
- name: Install dependencies
run: bun install
- name: Run TypeScript tests
run: bun test
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun tstest
- name: Run Terraform tests
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun tftest
- name: Run Terraform Validate
env:
ALL_CHANGED_FILES: ${{ steps.filter.outputs.all_files }}
SHARED_CHANGED: ${{ steps.filter.outputs.shared }}
MODULE_CHANGED_FILES: ${{ steps.filter.outputs.modules_files }}
run: bun terraform-validate
validate-style:
name: Check for typos and unformatted code
@@ -48,7 +82,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.35.5
uses: crate-ci/typos@v1.38.1
with:
config: .github/typos.toml
validate-readme-files:
@@ -61,7 +95,7 @@ jobs:
- name: Check out code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.23.2"
- name: Validate contributors
+2 -2
View File
@@ -30,12 +30,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@26f734c2779b00b7dda794207734c511110a4368
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db
- name: Deploy to dev.registry.coder.com
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
- name: Deploy to registry.coder.com
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version: stable
- name: golangci-lint
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
- name: Comment on PR - Failure
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Akamai</title>
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512pt" height="512pt" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="m500.48 262.2-48.18 73.984c-0.73438 1.1367-2 1.8242-3.3555 1.8242-1.3516 0-2.6172-0.6875-3.3516-1.8242l-48.129-73.984c-0.78125-1.2227-0.83594-2.7773-0.14453-4.0547 0.69141-1.2734 2.0195-2.0742 3.4727-2.0898h24.781c-0.007813-29.523-7.7188-58.531-22.375-84.156-14.652-25.629-35.742-46.988-61.184-61.969-2.3711-1.3633-3.8633-3.8594-3.9453-6.5938-0.085937-2.7305 1.2539-5.3125 3.5352-6.8203l27.035-17.613c3.4766-2.3633 8.043-2.3633 11.52 0 28.473 19.934 51.723 46.441 67.773 77.27 16.051 30.828 24.434 65.074 24.438 99.832h24.781c1.4688 0 2.8203 0.80859 3.5156 2.1055 0.69531 1.293 0.62109 2.8633-0.1875 4.0898zm-85.043 79.359c-1.5078-2.2812-4.0898-3.6211-6.8203-3.5391-2.7344 0.085937-5.2305 1.5781-6.5938 3.9492-14.965 25.434-36.305 46.523-61.914 61.188-25.609 14.664-54.602 22.391-84.109 22.422v-24.781c-0.011719-1.4531-0.8125-2.7812-2.0898-3.4727-1.2773-0.69141-2.832-0.63672-4.0547 0.14453l-74.035 47.977c-1.1367 0.73438-1.8242 1.9961-1.8242 3.3516s0.6875 2.6172 1.8242 3.3555l73.984 48.18c1.2227 0.78125 2.7773 0.83594 4.0547 0.14453 1.2734-0.69141 2.0742-2.0234 2.0898-3.4727v-24.68c34.734-0.015624 68.957-8.3984 99.766-24.441 30.812-16.039 57.301-39.27 77.23-67.719 2.3672-3.4766 2.3672-8.043 0-11.52zm-245.45 60.52c-25.434-14.977-46.516-36.328-61.172-61.945-14.652-25.617-22.371-54.617-22.387-84.129h24.781c1.4531-0.011719 2.7812-0.8125 3.4727-2.0898 0.69141-1.2773 0.63672-2.832-0.14453-4.0547l-47.977-74.035c-0.73438-1.1367-1.9961-1.8242-3.3516-1.8242s-2.6172 0.6875-3.3555 1.8242l-48.332 73.984c-0.80859 1.2266-0.88281 2.7969-0.1875 4.0898 0.69531 1.2969 2.0469 2.1055 3.5156 2.1055h24.781c0.015625 34.734 8.3984 68.957 24.438 99.766 16.043 30.812 39.273 57.301 67.723 77.234 3.4766 2.3633 8.043 2.3633 11.52 0l27.086-17.664c2.2109-1.5195 3.4961-4.0625 3.4141-6.7422-0.082032-2.6836-1.5234-5.1406-3.8242-6.5195zm92.16-390.5c-1.2227-0.78125-2.7773-0.83594-4.0547-0.14453-1.2773 0.69141-2.0781 2.0195-2.0898 3.4727v24.73c-34.734 0.015625-68.957 8.3984-99.766 24.438-30.812 16.043-57.301 39.273-77.234 67.723-2.3633 3.4766-2.3633 8.043 0 11.52l17.664 27.086c1.5078 2.2812 4.0898 3.6211 6.8242 3.5352 2.7305-0.082032 5.2266-1.5742 6.5898-3.9453 14.965-25.41 36.289-46.48 61.879-61.133 25.59-14.652 54.555-22.383 84.043-22.426v24.781c0.011719 1.4531 0.8125 2.7812 2.0898 3.4727 1.2773 0.69141 2.832 0.63672 4.0547-0.14453l74.035-47.977c1.1367-0.73438 1.8242-1.9961 1.8242-3.3516s-0.6875-2.6172-1.8242-3.3555zm-6.1445 210.23c-9.0703 0-17.77 3.6055-24.184 10.02-6.4141 6.4141-10.02 15.113-10.02 24.184s3.6055 17.77 10.02 24.184c6.4141 6.4141 15.113 10.02 24.184 10.02s17.77-3.6055 24.184-10.02c6.4141-6.4141 10.02-15.113 10.02-24.184s-3.6055-17.77-10.02-24.184c-6.4141-6.4141-15.113-10.02-24.184-10.02zm90.727-26.828-10.344 14.953c4.0039 6.9414 7.0859 14.375 9.1641 22.117l17.973 2.9688c6.543 1.1445 11.316 6.8242 11.316 13.465v15.055c0 6.6406-4.7734 12.32-11.316 13.465l-17.766 3.125v-0.003907c-2.1562 7.6992-5.3086 15.082-9.3711 21.965l10.238 14.797h0.003906c3.8047 5.4375 3.1562 12.82-1.5352 17.512l-10.648 10.648h-0.003906c-4.6914 4.6953-12.074 5.3438-17.508 1.5391l-14.797-10.238v-0.003907c-6.9453 4.0039-14.379 7.0859-22.121 9.1641l-3.0195 18.023c-1.1445 6.543-6.8242 11.316-13.465 11.316h-15.055c-6.6406 0-12.32-4.7734-13.465-11.316l-3.125-17.766h0.003907c-7.7031-2.1758-15.086-5.3398-21.965-9.4219l-14.797 10.238v0.003907c-5.4375 3.8047-12.82 3.1562-17.512-1.5391l-10.648-10.648c-4.6953-4.6914-5.3438-12.074-1.5391-17.512l10.238-14.797h0.003907c-4.0039-6.9414-7.0859-14.375-9.1641-22.117l-18.023-2.9688c-6.543-1.1445-11.316-6.8242-11.316-13.465v-15.055c0-6.6406 4.7734-12.32 11.316-13.465l17.766-3.125v0.003907c2.1562-7.6992 5.3086-15.082 9.3711-21.965l-10.238-14.797h-0.003906c-3.8047-5.4375-3.1562-12.82 1.5352-17.512l10.648-10.648h0.003906c4.6914-4.6953 12.074-5.3438 17.508-1.5391l14.797 10.238v0.003907c6.9453-4.0039 14.379-7.0859 22.121-9.1641l3.0195-18.023c1.1445-6.543 6.8242-11.316 13.465-11.316h15.055c6.6406 0 12.32 4.7734 13.465 11.316l3.125 17.766h-0.003907c7.6992 2.1562 15.082 5.3086 21.965 9.3711l14.797-10.238v-0.003906c5.4375-3.8047 12.82-3.1562 17.512 1.5352l10.648 10.648v0.003906c4.6875 4.6367 5.3984 11.957 1.6914 17.406zm-36.047 61.031c0-14.504-5.7578-28.41-16.016-38.664-10.254-10.258-24.16-16.016-38.664-16.016s-28.41 5.7578-38.664 16.016c-10.258 10.254-16.016 24.16-16.016 38.664s5.7578 28.41 16.016 38.664c10.254 10.258 24.16 16.016 38.664 16.016 14.5-0.011719 28.398-5.7773 38.652-16.027 10.25-10.254 16.016-24.152 16.027-38.652z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+210
View File
@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300mm"
height="207mm"
viewBox="0 0 300 207"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="15"
y1="15"
x2="15"
y2="143"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett">
<rect
style="fill:#333333"
id="rect1"
width="300"
height="205"
x="0"
y="0"
rx="12"
ry="12" />
<rect
style="fill:url(#linearGradient2)"
id="rect2"
width="270"
height="128"
x="15"
y="15"
rx="8"
ry="8" />
<rect
style="fill:#333333"
id="rect3"
width="172"
height="52"
x="64"
y="72"
rx="26"
ry="26" />
<circle
style="fill:#cccccc"
id="circle1"
cx="91"
cy="98"
r="18" />
<circle
style="fill:#cccccc"
id="circle2"
cx="209"
cy="98"
r="18" />
<path
style="fill:#737373;stroke-width:1px"
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="tekst"
style="display:none">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.153069"
y="55.548954"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.153069"
y="55.548954"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt">
<path
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
style="fill:#333333"
id="path11" />
<path
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
style="fill:#333333"
id="path12" />
<path
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path13" />
<path
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
style="fill:#333333"
id="path14" />
<path
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path15" />
<path
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
style="fill:#333333"
id="path16" />
<path
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
style="fill:#333333"
id="path17" />
<path
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
style="fill:#333333"
id="path18" />
<path
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
style="fill:#333333"
id="path19" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger">
<g
id="g1">
<path
id="path4"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2"
transform="rotate(30,150,318.19)">
<path
id="path7"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>

After

Width:  |  Height:  |  Size: 289 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="251" height="251" viewBox="0 0 251 251" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 47.0195C39.45 49.6394 71.06 81.3272 73.54 120.815H119.61C117.05 55.8589 64.93 3.64245 0 0.942627V47.0195Z" fill="#0DC09D"/>
<path d="M73.8 131.324C71.18 170.771 39.49 202.379 0 204.859V250.926C64.96 248.366 117.18 196.249 119.88 131.324H73.8Z" fill="#0DC09D"/>
<path d="M176.201 120.545C178.821 81.0972 210.511 49.4894 250.001 47.0095V0.942627C185.041 3.50245 132.821 55.619 130.121 120.545H176.201Z" fill="#0DC09D"/>
<path d="M250.001 204.849C210.551 202.229 178.941 170.542 176.461 131.054H130.391C132.951 196.01 185.071 248.226 250.001 250.926V204.849Z" fill="#0DC09D"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="32" viewBox="0 0 375 375" width="32" xmlns="http://www.w3.org/2000/svg">
<rect fill="#0071ff" height="375.001088" rx="58.59392" stroke-width=".91553" width="375.001088" x=".0009759" y="-.0066962"/>
<path d="m150.428 322.264c-29.063-6.202-53.897-22.439-73.115-47.804-19.507-25.746-27.838-55.355-25.723-91.414 6.655-62.013 47.667-106.753 99.687-120.411 4.509-.989 8.353-3.462 12.55-1.322 3.22 1.64 6.028 4.467 7.206 7.251 1.25 2.955 1.877 21.54.99 29.331-1.076 9.46-3.877 12.418-14.566 15.388-29.723 10.195-48.105 34.07-53.697 61.017-4.8 29.668 2.951 59.729 21.528 78.727 8.966 8.993 17.92 14.24 30.869 18.086 8.646 2.57 13.393 5.758 15.036 10.102 1.085 2.867 1.63 22.984.779 28.772-1.33 9.046-1.702 9.796-5.792 11.667-5.029 2.3-7.404 2.392-15.752.61zm50.708.29c-3.092-1.402-5.673-4.83-6.73-8.94-.134-9.408-2.366-25.754 1.02-33.373 1.88-4.128 4.65-5.999 12.433-8.396 21.267-6.551 37.593-19.88 46.806-38.213 11.11-22.108 11.877-55.183 1.808-77.975-9.154-20.723-25.7-35.217-48.555-42.534-8.872-2.84-12.004-5.065-12.968-9.21-1.002-4.31-1.435-19.87-.785-28.218.682-8.766 1.249-9.99 6.162-13.318 3.701-2.505 5.482-2.446 17.223.575 36.718 10.077 65.97 33.597 83.026 66.68 18.495 37.034 19.191 86.11 1.742 122.655-17.233 36.09-50.591 62.511-88.622 70.194-8.172 1.65-9.07 1.656-12.56.073z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+19 -8
View File
@@ -124,18 +124,23 @@ This script generates:
- Accurate description and usage examples
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
- Proper tags that describe your module
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
3. **Create tests for your module:**
- **Terraform tests**: Create a `*.tftest.hcl` file and test with `terraform test`
- **TypeScript tests**: Create `main.test.ts` file if your module runs scripts or has business logic that Terraform tests can't cover
4. **Add any scripts** or additional files your module needs
### 4. Test and Submit
```bash
# Test your module (from the module directory)
# Test your module
cd registry/[namespace]/modules/[module-name]
# Required: Test Terraform functionality
terraform init -upgrade
terraform test -verbose
# Or run all tests in the repo
./scripts/terraform_test_all.sh
# Optional: Test TypeScript files if you have main.test.ts
bun test main.test.ts
# Format code
bun run fmt
@@ -343,8 +348,8 @@ coder templates push test-[template-name] -d .
terraform init -upgrade
terraform test -verbose
# Test all modules
./scripts/terraform_test_all.sh
# Optional: If you have TypeScript tests
bun test main.test.ts
```
### 3. Maintain Backward Compatibility
@@ -393,7 +398,9 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
### Every Module Must Have
- `main.tf` - Terraform code
- One or more `.tftest.hcl` files - Working tests with `terraform test`
- **Tests**:
- `*.tftest.hcl` files with `terraform test` (to test terraform specific logic)
- `main.test.ts` file with `bun test` (to test business logic, i.e., `coder_script` to install a package.)
- `README.md` - Documentation with frontmatter
### Every Template Must Have
@@ -493,6 +500,10 @@ When reporting bugs, include:
2. **No tests** or broken tests
3. **Hardcoded values** instead of variables
4. **Breaking changes** without defaults
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`, and `bun test main.test.ts` if applicable) before submitting
## For Maintainers
Guidelines for reviewing PRs, managing releases, and maintaining the registry. [See the maintainer guide for detailed information.](./MAINTAINER.md)
Happy contributing! 🚀
+3 -1
View File
@@ -23,6 +23,7 @@ Check that PRs have:
- [ ] Working tests (`terraform test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
- [ ] Version label: `version:patch`, `version:minor`, or `version:major`
### Version Guidelines
@@ -32,7 +33,8 @@ When reviewing PRs, ensure the version change follows semantic versioning:
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
PRs should clearly indicate the intended version change (e.g., `v1.2.3 → v1.2.4`) and include the appropriate label: `version:patch`, `version:minor`, or `version:major`.
The “Version Bump” CI uses this label to validate required updates (README version refs, etc.).
### Validate READMEs
+4
View File
@@ -48,3 +48,7 @@ Simply include that snippet inside your Coder template, defining any data depend
## Contributing
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
## For Maintainers
Guidelines for maintainers reviewing PRs and managing releases. [See the maintainer guide for more information.](./MAINTAINER.md)
+2 -2
View File
@@ -15,7 +15,7 @@ run "app_url_uses_port" {
}
assert {
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
error_message = "Expected MODULE_NAME app URL to include configured port"
condition = resource.coder_app.module_name.url == "http://localhost:19999"
error_message = "Expected module-name app URL to include configured port"
}
}
+12 -12
View File
@@ -35,13 +35,13 @@ variable "agent_id" {
variable "log_path" {
type = string
description = "The path to log MODULE_NAME to."
default = "/tmp/MODULE_NAME.log"
description = "The path to the module log file."
default = "/tmp/module_name.log"
}
variable "port" {
type = number
description = "The port to run MODULE_NAME on."
description = "The port to run the application on."
default = 19999
}
@@ -59,9 +59,9 @@ variable "order" {
# Add other variables here
resource "coder_script" "MODULE_NAME" {
resource "coder_script" "module_name" {
agent_id = var.agent_id
display_name = "MODULE_NAME"
display_name = "Module Name"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
@@ -70,10 +70,10 @@ resource "coder_script" "MODULE_NAME" {
run_on_stop = false
}
resource "coder_app" "MODULE_NAME" {
resource "coder_app" "module_name" {
agent_id = var.agent_id
slug = "MODULE_NAME"
display_name = "MODULE_NAME"
slug = "module-name"
display_name = "Module Name"
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = false
@@ -88,10 +88,10 @@ resource "coder_app" "MODULE_NAME" {
}
}
data "coder_parameter" "MODULE_NAME" {
type = "list(string)"
name = "MODULE_NAME"
display_name = "MODULE_NAME"
data "coder_parameter" "module_name" {
type = "string"
name = "module_name"
display_name = "Module Name"
icon = local.icon_url
mutable = var.mutable
default = local.options["Option 1"]["value"]
+2 -1
View File
@@ -4,7 +4,8 @@
"fmt": "bun x prettier --write . && terraform fmt -recursive -diff",
"fmt:ci": "bun x prettier --check . && terraform fmt -check -recursive -diff",
"terraform-validate": "./scripts/terraform_validate.sh",
"test": "./scripts/terraform_test_all.sh",
"tftest": "./scripts/terraform_test_all.sh",
"tstest": "./scripts/ts_test_auto.sh",
"update-version": "./update-version.sh"
},
"devDependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

+1 -1
View File
@@ -16,7 +16,7 @@ It can be served on a Coder subdomain for easy access, or on `localhost` if you
```tf
module "pgadmin" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/aj0070/pgadmin/coder"
source = "registry.coder.com/AJ0070/pgadmin/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+14
View File
@@ -0,0 +1,14 @@
---
display_name: "Benraouane Soufiane"
bio: "Full stack developer creating awesome things."
avatar: "./.images/avatar.png"
github: "benraouanesoufiane"
linkedin: "https://www.linkedin.com/in/benraouane-soufiane" # Optional
website: "https://benraouanesoufiane.com" # Optional
support_email: "hello@benraouanesoufiane.com" # Optional
status: "community"
---
# Benraouane Soufiane
Full stack developer creating awesome things.
@@ -0,0 +1,82 @@
---
display_name: RustDesk
description: Run RustDesk in your workspace with virtual display
icon: ../../../../.icons/rustdesk.svg
verified: false
tags: [rustdesk, rdp, vm]
---
# RustDesk
Launches RustDesk within your workspace with a virtual display to provide remote desktop access. The module outputs the RustDesk ID and password needed to connect from external RustDesk clients.
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
## Features
- Automatically sets up virtual display (Xvfb)
- Downloads and configures RustDesk
- Outputs RustDesk ID and password for easy connection
- Provides external app link to RustDesk web client for browser-based access
- Starts virtual display (Xvfb) with customizable resolution
- Customizable screen resolution and RustDesk version
## Requirements
- Coder v2.5 or higher
- Linux workspace with `apt`, `dnf`, or `yum` package manager
## Examples
### Custom configuration with specific version
```tf
module "rustdesk" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/BenraouaneSoufiane/rustdesk/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
rustdesk_password = "mycustompass"
xvfb_resolution = "1920x1080x24"
rustdesk_version = "1.4.1"
}
```
### Docker container configuration
It requires coder' server to be run as root, when using with Docker, add the following to your `docker_container` resource:
```tf
resource "docker_container" "workspace" {
# ... other configuration ...
user = "root"
privileged = true
network_mode = "host"
ports {
internal = 21115
external = 21115
}
ports {
internal = 21116
external = 21116
}
ports {
internal = 21118
external = 21118
}
ports {
internal = 21119
external = 21119
}
}
```
@@ -0,0 +1,75 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "log_path" {
type = string
description = "The path to log rustdesk to."
default = "/tmp/rustdesk.log"
}
variable "agent_id" {
description = "Attach RustDesk setup to this agent"
type = string
}
variable "order" {
description = "Run order among scripts/apps"
type = number
default = 1
}
# Optional knobs passed as env (you can expose these as variables too)
variable "rustdesk_password" {
description = "If empty, the script will generate one"
type = string
default = ""
sensitive = true
}
variable "xvfb_resolution" {
description = "Xvfb screen size/depth"
type = string
default = "1024x768x16"
}
variable "rustdesk_version" {
description = "RustDesk version to install (use 'latest' for most recent release)"
type = string
default = "latest"
}
resource "coder_script" "rustdesk" {
agent_id = var.agent_id
display_name = "RustDesk"
run_on_start = true
# Prepend env as bash exports, then append the script file literally.
script = <<-EOT
# --- module-provided env knobs ---
export RUSTDESK_PASSWORD="${var.rustdesk_password}"
export XVFB_RESOLUTION="${var.xvfb_resolution}"
export RUSTDESK_VERSION="${var.rustdesk_version}"
# ---------------------------------
${file("${path.module}/run.sh")}
EOT
}
resource "coder_app" "rustdesk" {
agent_id = var.agent_id
slug = "rustdesk"
display_name = "Rustdesk"
url = "https://rustdesk.com/web"
icon = "/icon/rustdesk.svg"
order = var.order
external = true
}
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
RESET='\033[0m'
printf "${BOLD}🖥️ Installing RustDesk Remote Desktop\n${RESET}"
# ---- configurable knobs (env overrides) ----
RUSTDESK_VERSION="${RUSTDESK_VERSION:-latest}"
LOG_PATH="${LOG_PATH:-/tmp/rustdesk.log}"
# ---- fetch latest version if needed ----
if [ "$RUSTDESK_VERSION" = "latest" ]; then
printf "🔍 Fetching latest RustDesk version...\n"
RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' || echo "1.4.1")
printf "📌 Fetched RustDesk version: ${RUSTDESK_VERSION}\n"
else
printf "📌 Using specified RustDesk version: ${RUSTDESK_VERSION}\n"
fi
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"
RUSTDESK_PASSWORD="${RUSTDESK_PASSWORD:-}"
# ---- detect package manager & arch ----
ARCH="$(uname -m)"
case "$ARCH" in
x86_64 | amd64) PKG_ARCH="x86_64" ;;
aarch64 | arm64) PKG_ARCH="aarch64" ;;
*)
echo "❌ Unsupported arch: $ARCH"
exit 1
;;
esac
if command -v apt-get > /dev/null 2>&1; then
PKG_SYS="deb"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.deb"
INSTALL_DEPS='apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y wget libva2 libva-drm2 libva-x11-2 libgstreamer-plugins-base1.0-0 gstreamer1.0-pipewire xfce4 xfce4-goodies xvfb x11-xserver-utils dbus-x11 libegl1 libgl1 libglx0 libglu1-mesa mesa-utils libxrandr2 libxss1 libgtk-3-0t64 libgbm1 libdrm2 libxcomposite1 libxdamage1 libxfixes3'
INSTALL_CMD="apt-get install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v dnf > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='dnf install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="dnf install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
elif command -v yum > /dev/null 2>&1; then
PKG_SYS="rpm"
PKG_NAME="rustdesk-${RUSTDESK_VERSION}-${PKG_ARCH}.rpm"
INSTALL_DEPS='yum install -y wget libva libva-intel-driver gstreamer1-plugins-base pipewire xfce4-session xfce4-panel xorg-x11-server-Xvfb xorg-x11-xauth dbus-x11 mesa-libEGL mesa-libGL mesa-libGLU mesa-dri-drivers libXrandr libXScrnSaver gtk3 mesa-libgbm libdrm libXcomposite libXdamage libXfixes'
INSTALL_CMD="yum install -y ./${PKG_NAME}"
CLEAN_CMD="rm -f \"${PKG_NAME}\""
else
echo "❌ Unsupported distro: need apt, dnf, or yum."
exit 1
fi
# ---- install rustdesk if missing ----
if ! command -v rustdesk > /dev/null 2>&1; then
printf "📦 Installing dependencies...\n"
sudo bash -c "$INSTALL_DEPS" 2>&1 | tee -a "${LOG_PATH}"
printf "⬇️ Downloading RustDesk ${RUSTDESK_VERSION} (${PKG_SYS}, ${PKG_ARCH})...\n"
URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/${PKG_NAME}"
wget -q "$URL" 2>&1 | tee -a "${LOG_PATH}"
printf "🔧 Installing RustDesk...\n"
sudo bash -c "$INSTALL_CMD" 2>&1 | tee -a "${LOG_PATH}"
printf "🧹 Cleaning up...\n"
bash -c "$CLEAN_CMD" 2>&1 | tee -a "${LOG_PATH}"
else
printf "✅ RustDesk already installed\n"
fi
# ---- start virtual display ----
echo "Starting Xvfb with resolution ${XVFB_RESOLUTION}"
Xvfb :99 -screen 0 "${XVFB_RESOLUTION}" >> "${LOG_PATH}" 2>&1 &
export DISPLAY=:99
# Wait for X to be ready
for i in {1..10}; do
if xdpyinfo -display :99 > /dev/null 2>&1; then
echo "X display is ready"
break
fi
sleep 1
done
# ---- create (or accept) password and start rustdesk ----
if [[ -z "${RUSTDESK_PASSWORD}" ]]; then
RUSTDESK_PASSWORD="$(tr -dc 'a-zA-Z0-9@' < /dev/urandom | head -c 10)@97"
fi
echo "Starting XFCE desktop environment..."
xfce4-session >> "${LOG_PATH}" 2>&1 &
echo "Waiting for xfce4-session to initialize..."
sleep 5
printf "🔐 Setting RustDesk password and starting service...\n"
rustdesk >> "${LOG_PATH}" 2>&1 &
sleep 2
rustdesk --password "${RUSTDESK_PASSWORD}" >> "${LOG_PATH}" 2>&1 &
sleep 3
RID="$(rustdesk --get-id 2> /dev/null || echo 'ID_PENDING')"
printf "🥳 RustDesk setup complete!\n\n"
printf "${BOLD}📋 Connection Details:${RESET}\n"
printf " RustDesk ID: ${RID}\n"
printf " RustDesk Password: ${RUSTDESK_PASSWORD}\n"
printf " Display: ${DISPLAY} (${XVFB_RESOLUTION})\n"
printf "\n📝 Logs available at: ${LOG_PATH}\n\n"
echo "Setup script completed successfully. All services running in background."
exit 0
@@ -0,0 +1,163 @@
---
display_name: Archive
description: Create automated and user-invocable scripts that archive and extract selected files/directories with optional compression (gzip or zstd).
icon: ../../../../.icons/folder.svg
verified: false
tags: [backup, archive, tar, helper]
---
# Archive
This module installs small, robust scripts in your workspace to create and extract tar archives from a list of files and directories. It supports optional compression (gzip or zstd). The create command prints only the resulting archive path to stdout; operational logs go to stderr. An optional stop hook can also create an archive automatically when the workspace stops, and an optional start hook can wait for an archive on-disk and extract it on start.
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
paths = ["./projects", "./code"]
}
```
## Features
- Installs two commands into the workspace `$PATH`: `coder-archive-create` and `coder-archive-extract`.
- Creates a single `.tar`, `.tar.gz`, or `.tar.zst` containing selected paths (depends on `tar`).
- Optional compression: `gzip`, `zstd` (depends on `gzip` or `zstd`).
- Stores defaults so commands can be run without arguments (supports overriding via CLI flags).
- Logs and status messages go to stderr, the create command prints only the final archive path to stdout.
- Optional:
- `create_on_stop` to create an archive automatically when the workspace stops.
- `extract_on_start` to wait for an archive to appear and extract it on start.
> [!WARNING]
> The `create_on_stop` feature uses the `coder_script` `run_on_stop` which may not work as expected on certain templates without additional provider configuration. The agent may be terminated before the script completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for provider-specific workarounds and [coder/coder#6175](https://github.com/coder/coder/issues/6175) for tracking a fix.
## Usage
Basic example:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Paths to include in the archive (files or directories).
directory = "~"
paths = [
"./projects",
"./code",
]
}
```
Customize compression and output:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
directory = "/"
paths = ["/etc", "/home"]
compression = "zstd" # "gzip" | "zstd" | "none"
output_dir = "/tmp/backup" # defaults to /tmp
archive_name = "my-backup" # base name (extension is inferred from compression)
}
```
Enable auto-archive on stop:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
create_on_stop = true
}
```
Extract on start:
```tf
module "archive" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/archive/coder"
version = "0.0.1"
agent_id = coder_agent.example.id
# Where to look for the archive file to extract:
output_dir = "/tmp"
archive_name = "my-archive"
compression = "gzip"
# Waits up to 5 minutes for /tmp/my-archive.tar.gz to be present, note that
# using a long timeout will delay every workspace start by this much until the
# archive is present.
extract_on_start = true
extract_wait_timeout_seconds = 300
}
```
## Command usage
The installer writes the following files:
- `$CODER_SCRIPT_DATA_DIR/archive-lib.sh`
- `$CODER_SCRIPT_BIN_DIR/coder-archive-create`
- `$CODER_SCRIPT_BIN_DIR/coder-archive-extract`
Create usage:
```console
coder-archive-create [OPTIONS] [PATHS...]
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
-C, --directory <DIRECTORY> Change to directory for archiving (default from module)
-f, --file <ARCHIVE> Output archive file (default from module)
-h, --help Show help
```
Extract usage:
```console
coder-archive-extract [OPTIONS]
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
-C, --directory <DIRECTORY> Extract into directory (default from module)
-f, --file <ARCHIVE> Archive file to extract (default from module)
-h, --help Show help
```
Examples:
- Use Terraform defaults:
```
coder-archive-create
```
- Override compression and output file at runtime:
```
coder-archive-create --compression zstd --file /tmp/backups/archive.tar.zst
```
- Add extra paths on the fly (in addition to the Terraform defaults):
```
coder-archive-create /etc/hosts
```
- Extract an archive into a directory:
```
coder-archive-extract --file /tmp/backups/archive.tar.gz --directory /tmp/restore
```
@@ -0,0 +1,33 @@
mock_provider "coder" {}
run "apply_defaults" {
command = apply
variables {
agent_id = "agent-123"
paths = ["~/project", "/etc/hosts"]
}
assert {
condition = output.archive_path == "/tmp/coder-archive.tar.gz"
error_message = "archive_path should be empty when archive_name is not set"
}
}
run "apply_with_name" {
command = apply
variables {
agent_id = "agent-123"
paths = ["/etc/hosts"]
archive_name = "nightly"
output_dir = "/tmp/backups"
compression = "zstd"
create_archive_on_stop = true
}
assert {
condition = output.archive_path == "/tmp/backups/nightly.tar.zst"
error_message = "archive_path should be computed from archive_name + output_dir + extension"
}
}
@@ -0,0 +1,348 @@
import { describe, expect, it, beforeAll } from "bun:test";
import {
execContainer,
findResourceInstance,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
type TerraformState,
} from "~test";
const USE_XTRACE =
process.env.ARCHIVE_TEST_XTRACE === "1" || process.env.XTRACE === "1";
const IMAGE = "alpine";
const BIN_DIR = "/tmp/coder-script-data/bin";
const DATA_DIR = "/tmp/coder-script-data";
type ExecResult = {
exitCode: number;
stdout: string;
stderr: string;
};
const ensureRunOk = (label: string, res: ExecResult) => {
if (res.exitCode !== 0) {
console.error(
`[${label}] non-zero exit code: ${res.exitCode}\n--- stdout ---\n${res.stdout.trim()}\n--- stderr ---\n${res.stderr.trim()}\n--------------`,
);
}
expect(res.exitCode).toBe(0);
};
const sh = async (id: string, cmd: string): Promise<ExecResult> => {
const res = await execContainer(id, ["sh", "-c", cmd]);
return res;
};
const bashRun = async (id: string, cmd: string): Promise<ExecResult> => {
const injected = USE_XTRACE ? `/bin/bash -x ${cmd}` : cmd;
return sh(id, injected);
};
const prepareContainer = async (image = IMAGE) => {
const id = await runContainer(image);
// Prepare script dirs and deps.
ensureRunOk(
"mkdirs",
await sh(id, `mkdir -p ${BIN_DIR} ${DATA_DIR} /tmp/backup`),
);
// Install tools used by tests.
ensureRunOk(
"apk add",
await sh(id, "apk add --no-cache bash tar gzip zstd coreutils"),
);
return id;
};
const installArchive = async (
state: TerraformState,
opts?: { env?: string[] },
) => {
const instance = findResourceInstance(state, "coder_script");
const id = await prepareContainer();
// Run installer script with correct env for CODER_SCRIPT paths.
const args = ["bash"];
if (USE_XTRACE) args.push("-x");
args.push("-c", instance.script);
const resp = await execContainer(id, args, [
"--env",
`CODER_SCRIPT_BIN_DIR=${BIN_DIR}`,
"--env",
`CODER_SCRIPT_DATA_DIR=${DATA_DIR}`,
...(opts?.env ?? []),
]);
return {
id,
install: {
exitCode: resp.exitCode,
stdout: resp.stdout.trim(),
stderr: resp.stderr.trim(),
},
};
};
const fileExists = async (id: string, path: string) => {
const res = await sh(id, `test -f ${path} && echo yes || echo no`);
return res.stdout.trim() === "yes";
};
const isExecutable = async (id: string, path: string) => {
const res = await sh(id, `test -x ${path} && echo yes || echo no`);
return res.stdout.trim() === "yes";
};
const listTar = async (id: string, path: string) => {
// Try to autodetect compression flags from extension.
let cmd = "";
if (path.endsWith(".tar.gz")) {
cmd = `tar -tzf ${path}`;
} else if (path.endsWith(".tar.zst")) {
// validate with zstd and ask tar to list via --zstd.
cmd = `zstd -t -q ${path} && tar --zstd -tf ${path}`;
} else {
cmd = `tar -tf ${path}`;
}
return sh(id, cmd);
};
describe("archive", () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
// Ensure required variables are enforced.
testRequiredVariables(import.meta.dir, {
agent_id: "agent-123",
});
it("installs wrapper scripts to BIN_DIR and library to DATA_DIR", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
});
// The Terraform output should reflect defaults from main.tf.
expect(state.outputs.archive_path.value).toEqual(
"/tmp/coder-archive.tar.gz",
);
const { id, install } = await installArchive(state);
ensureRunOk("install", install);
expect(install.stdout).toContain(
`Installed archive library to: ${DATA_DIR}/archive-lib.sh`,
);
expect(install.stdout).toContain(
`Installed create script to: ${BIN_DIR}/coder-archive-create`,
);
expect(install.stdout).toContain(
`Installed extract script to: ${BIN_DIR}/coder-archive-extract`,
);
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-create`)).toBe(
true,
);
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-extract`)).toBe(
true,
);
});
it("uses sane defaults: creates gzip archive at the default path and logs to stderr", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Keep defaults: compression=gzip, output_dir=/tmp, archive_name=coder-archive.
});
const { id } = await installArchive(state);
const createTestdata = await bashRun(
id,
`mkdir ~/gzip; touch ~/gzip/defaults.txt`,
);
ensureRunOk("create testdata", createTestdata);
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
ensureRunOk("archive-create default run", run);
// Only the archive path should print to stdout.
expect(run.stdout.trim()).toEqual("/tmp/coder-archive.tar.gz");
expect(await fileExists(id, "/tmp/coder-archive.tar.gz")).toBe(true);
// Some useful diagnostics should be on stderr.
expect(run.stderr).toContain("Creating archive:");
expect(run.stderr).toContain("Compression: gzip");
const list = await listTar(id, "/tmp/coder-archive.tar.gz");
ensureRunOk("list default archive", list);
expect(list.stdout).toContain("gzip/defaults.txt");
}, 20000);
it("creates a gzip archive with explicit -f and includes extra CLI paths", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Provide a simple default path so we can assert contents.
paths: `["~/gzip"]`,
compression: "gzip",
});
const { id } = await installArchive(state);
const createTestdata = await bashRun(
id,
`mkdir ~/gzip; touch ~/gzip/test.txt; touch ~/gziptest.txt`,
);
ensureRunOk("create testdata", createTestdata);
const out = "/tmp/backup/test-archive.tar.gz";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create -f ${out} ~/gziptest.txt`,
);
ensureRunOk("archive-create gzip explicit -f", run);
expect(run.stdout.trim()).toEqual(out);
expect(await fileExists(id, out)).toBe(true);
const list = await sh(id, `tar -tzf ${out}`);
ensureRunOk("tar -tzf contents (gzip)", list);
expect(list.stdout).toContain("gzip/test.txt");
expect(list.stdout).toContain("gziptest.txt");
}, 20000);
it("creates a zstd-compressed archive when requested via CLI override", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
// Module default is gzip, override at runtime to zstd.
});
const { id } = await installArchive(state);
const out = "/tmp/backup/zstd-archive.tar.zst";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create --compression zstd -f ${out}`,
);
ensureRunOk("archive-create zstd", run);
expect(run.stdout.trim()).toEqual(out);
// Check integrity via zstd and that tar can list it.
ensureRunOk("zstd -t", await sh(id, `test -f ${out} && zstd -t -q ${out}`));
ensureRunOk("tar --zstd -tf", await sh(id, `tar --zstd -tf ${out}`));
}, 30000);
it("creates an uncompressed tar when compression=none", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
// Keep module defaults but override at runtime.
});
const { id } = await installArchive(state);
const out = "/tmp/backup/raw-archive.tar";
const run = await bashRun(
id,
`${BIN_DIR}/coder-archive-create --compression none -f ${out}`,
);
ensureRunOk("archive-create none", run);
expect(run.stdout.trim()).toEqual(out);
ensureRunOk("tar -tf (none)", await sh(id, `tar -tf ${out} >/dev/null`));
}, 20000);
it("applies exclude patterns from Terraform", async () => {
// Include a file, but also exclude it via Terraform defaults to ensure
// exclusion flows through.
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
exclude_patterns: `["/etc/hostname"]`,
});
const { id } = await installArchive(state);
const out = "/tmp/backup/excluded.tar.gz";
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create -f ${out}`);
ensureRunOk("archive-create with exclude_patterns", run);
const list = await sh(id, `tar -tzf ${out}`);
ensureRunOk("tar -tzf contents (exclude)", list);
expect(list.stdout).not.toContain("etc/hostname"); // Excluded by Terraform default.
}, 20000);
it("adds a run_on_stop script when enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
create_on_stop: true,
});
const coderScripts = state.resources.filter(
(r) => r.type === "coder_script",
);
// Installer (run_on_start) + run_on_stop.
expect(coderScripts.length).toBe(2);
});
it("extracts a previously created archive into a target directory", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
paths: `["/etc/hostname"]`,
compression: "gzip",
});
const { id } = await installArchive(state);
// Create archive.
const out = "/tmp/backup/extract-test.tar.gz";
const created = await bashRun(
id,
`${BIN_DIR}/coder-archive-create -f ${out} /etc/hosts`,
);
ensureRunOk("create for extract", created);
// Extract archive.
const extractDir = "/tmp/extract";
const extract = await bashRun(
id,
`${BIN_DIR}/coder-archive-extract -f ${out} -C ${extractDir}`,
);
ensureRunOk("archive-extract", extract);
// Verify a known file exists after extraction.
const exists = await sh(
id,
`test -f ${extractDir}/etc/hosts && echo ok || echo no`,
);
expect(exists.stdout.trim()).toEqual("ok");
}, 20000);
it("honors Terraform defaults without CLI args (compression, name, output_dir)", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "agent-123",
compression: "zstd",
archive_name: "my-default",
output_dir: "/tmp/defout",
});
const { id } = await installArchive(state);
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
ensureRunOk("archive-create terraform defaults", run);
expect(run.stdout.trim()).toEqual("/tmp/defout/my-default.tar.zst");
expect(run.stderr).toContain("Creating archive:");
expect(run.stderr).toContain("Compression: zstd");
ensureRunOk(
"zstd -t",
await sh(id, "zstd -t -q /tmp/defout/my-default.tar.zst"),
);
ensureRunOk(
"tar --zstd -tf",
await sh(id, "tar --zstd -tf /tmp/defout/my-default.tar.zst"),
);
}, 30000);
});
+134
View File
@@ -0,0 +1,134 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "agent_id" {
description = "The ID of a Coder agent."
type = string
}
variable "paths" {
description = "List of files/directories to include in the archive. Defaults to the current directory."
type = list(string)
default = ["."]
}
variable "exclude_patterns" {
description = "Exclude patterns for the archive."
type = list(string)
default = []
}
variable "compression" {
description = "Compression algorithm for the archive. Supported: gzip, zstd, none."
type = string
default = "gzip"
validation {
condition = contains(["gzip", "zstd", "none"], var.compression)
error_message = "compression must be one of: gzip, zstd, none."
}
}
variable "archive_name" {
description = "Optional archive base name without extension. If empty, defaults to \"coder-archive\"."
type = string
default = "coder-archive"
}
variable "output_dir" {
description = "Optional output directory where the archive will be written. Defaults to \"/tmp\"."
type = string
default = "/tmp"
}
variable "directory" {
description = "Change current directory to this path before creating or extracting the archive. Defaults to the user's home directory."
type = string
default = "~"
}
variable "create_on_stop" {
description = "If true, also create a run_on_stop script that creates the archive automatically on workspace stop."
type = bool
default = false
}
variable "extract_on_start" {
description = "If true, the installer will wait for an archive and extract it on start."
type = bool
default = false
}
variable "extract_wait_timeout_seconds" {
description = "Timeout (seconds) to wait for an archive when extract_on_start is true."
type = number
default = 5
}
# Provide a stable script filename and sensible defaults.
locals {
extension = var.compression == "gzip" ? ".tar.gz" : var.compression == "zstd" ? ".tar.zst" : ".tar"
# Ensure ~ is expanded because it cannot be expanded inside quotes in a
# templated shell script.
paths = [for v in var.paths : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
exclude_patterns = [for v in var.exclude_patterns : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
directory = replace(var.directory, "/^~(\\/|$)/", "$$HOME$1")
output_dir = replace(var.output_dir, "/^~(\\/|$)/", "$$HOME$1")
archive_path = "${local.output_dir}/${var.archive_name}${local.extension}"
}
output "archive_path" {
description = "Full path to the archive file that will be created, extracted, or both."
value = local.archive_path
}
# This script installs the user-facing archive script into $CODER_SCRIPT_BIN_DIR.
# The installed script can be run manually by the user to create an archive.
resource "coder_script" "archive_start_script" {
agent_id = var.agent_id
display_name = "Archive"
icon = "/icon/folder.svg"
run_on_start = true
start_blocks_login = var.extract_on_start
# Render the user-facing archive script with Terraform defaults, then write it to $CODER_SCRIPT_BIN_DIR
script = templatefile("${path.module}/run.sh", {
TF_LIB_B64 = base64encode(file("${path.module}/scripts/archive-lib.sh")),
TF_PATHS = join(" ", formatlist("%q", local.paths)),
TF_EXCLUDE_PATTERNS = join(" ", formatlist("%q", local.exclude_patterns)),
TF_COMPRESSION = var.compression,
TF_ARCHIVE_PATH = local.archive_path,
TF_DIRECTORY = local.directory,
TF_EXTRACT_ON_START = var.extract_on_start,
TF_EXTRACT_WAIT_TIMEOUT = var.extract_wait_timeout_seconds,
})
}
# Optionally, also register a run_on_stop script that creates the archive automatically
# when the workspace stops. It simply invokes the installed archive script.
resource "coder_script" "archive_stop_script" {
count = var.create_on_stop ? 1 : 0
agent_id = var.agent_id
display_name = "Archive"
icon = "/icon/folder.svg"
run_on_stop = true
start_blocks_login = false
# Call the installed script. It will log to stderr and print the archive path to stdout.
# We redirect stdout to stderr to avoid surfacing the path in system logs if undesired.
# Remove the redirection if you want the path to appear in stdout on stop as well.
script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
"$CODER_SCRIPT_BIN_DIR/coder-archive-create"
EOT
}
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
LIB_B64="${TF_LIB_B64}"
EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
# Set script defaults from Terraform.
DEFAULT_PATHS=(${TF_PATHS})
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
# 1) Decode the library into $CODER_SCRIPT_DATA_DIR/archive-lib.sh (static, sourceable).
LIB_PATH="$CODER_SCRIPT_DATA_DIR/archive-lib.sh"
lib_tmp="$(mktemp -t coder-module-archive.XXXXXX))"
trap 'rm -f "$lib_tmp" 2>/dev/null || true' EXIT
# Decode the base64 content safely.
if ! printf '%s' "$LIB_B64" | base64 -d > "$lib_tmp"; then
echo "ERROR: Failed to decode archive library from base64." >&2
exit 1
fi
chmod 0644 "$lib_tmp"
mv "$lib_tmp" "$LIB_PATH"
# 2) Generate the wrapper scripts (create and extract).
create_wrapper() {
tmp="$(mktemp -t coder-module-archive.XXXXXX)"
trap 'rm -f "$tmp" 2>/dev/null || true' EXIT
cat > "$tmp" << EOF
#!/usr/bin/env bash
set -euo pipefail
. "$LIB_PATH"
# Set defaults from Terraform (through installer).
$(
declare -p \
DEFAULT_PATHS \
DEFAULT_EXCLUDE_PATTERNS \
DEFAULT_COMPRESSION \
DEFAULT_ARCHIVE_PATH \
DEFAULT_DIRECTORY
)
$1 "\$@"
EOF
chmod 0755 "$tmp"
mv "$tmp" "$2"
}
CREATE_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-create"
EXTRACT_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-extract"
create_wrapper archive_create "$CREATE_WRAPPER_PATH"
create_wrapper archive_extract "$EXTRACT_WRAPPER_PATH"
echo "Installed archive library to: $LIB_PATH"
echo "Installed create script to: $CREATE_WRAPPER_PATH"
echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
# 3) Optionally wait for and extract an archive on start.
if [[ $EXTRACT_ON_START = true ]]; then
. "$LIB_PATH"
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
exit_code=$?
if [[ $exit_code -eq 2 ]]; then
echo "WARNING: Archive not found in backup path (this is expected with new workspaces)."
else
exit $exit_code
fi
}
fi
@@ -0,0 +1,279 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '%s\n' "$@" >&2
}
warn() {
printf 'WARNING: %s\n' "$1" >&2
}
error() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
load_defaults() {
DEFAULT_PATHS=("${DEFAULT_PATHS[@]:-.}")
DEFAULT_EXCLUDE_PATTERNS=("${DEFAULT_EXCLUDE_PATTERNS[@]:-}")
DEFAULT_COMPRESSION="${DEFAULT_COMPRESSION:-gzip}"
DEFAULT_ARCHIVE_PATH="${DEFAULT_ARCHIVE_PATH:-/tmp/coder-archive.tar.gz}"
DEFAULT_DIRECTORY="${DEFAULT_DIRECTORY:-$HOME}"
}
ensure_tools() {
command -v tar > /dev/null 2>&1 || error "tar is required"
case "$1" in
gzip)
command -v gzip > /dev/null 2>&1 || error "gzip is required for gzip compression"
;;
zstd)
command -v zstd > /dev/null 2>&1 || error "zstd is required for zstd compression"
;;
none) ;;
*)
error "Unsupported compression algorithm: $1"
;;
esac
}
usage_archive_create() {
load_defaults
cat >&2 << USAGE
Usage: coder-archive-create [OPTIONS] [[PATHS] ...]
Options:
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
-h, --help Show this help
USAGE
}
archive_create() {
load_defaults
local compression="${DEFAULT_COMPRESSION}"
local directory="${DEFAULT_DIRECTORY}"
local file="${DEFAULT_ARCHIVE_PATH}"
local paths=("${DEFAULT_PATHS[@]}")
while [[ $# -gt 0 ]]; do
case "$1" in
-c | --compression)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
compression="$2"
shift 2
;;
-C | --directory)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
directory="$2"
shift 2
;;
-f | --file)
if [[ $# -lt 2 ]]; then
usage_archive_create
error "Missing value for $1"
fi
file="$2"
shift 2
;;
-h | --help)
usage_archive_create
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
paths+=("$1")
shift
done
;;
-*)
usage_archive_create
error "Unknown option: $1"
;;
*)
paths+=("$1")
shift
;;
esac
done
ensure_tools "$compression"
local -a tar_opts=(-c -f "$file" -C "$directory")
case "$compression" in
gzip)
tar_opts+=(-z)
;;
zstd)
tar_opts+=(--zstd)
;;
none) ;;
*)
error "Unsupported compression algorithm: $compression"
;;
esac
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
if [[ -n $path ]]; then
tar_opts+=(--exclude "$path")
fi
done
# Ensure destination directory exists.
dest="$(dirname "$file")"
mkdir -p "$dest" 2> /dev/null || error "Failed to create output dir: $dest"
log "Creating archive:"
log " Compression: $compression"
log " Directory: $directory"
log " Archive: $file"
log " Paths: ${paths[*]}"
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
umask 077
tar "${tar_opts[@]}" "${paths[@]}"
printf '%s\n' "$file"
}
usage_archive_extract() {
load_defaults
cat >&2 << USAGE
Usage: coder-archive-extract [OPTIONS]
Options:
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
-h, --help Show this help
USAGE
}
archive_extract() {
load_defaults
local compression="${DEFAULT_COMPRESSION}"
local directory="${DEFAULT_DIRECTORY}"
local file="${DEFAULT_ARCHIVE_PATH}"
while [[ $# -gt 0 ]]; do
case "$1" in
-c | --compression)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
compression="$2"
shift 2
;;
-C | --directory)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
directory="$2"
shift 2
;;
-f | --file)
if [[ $# -lt 2 ]]; then
usage_archive_extract
error "Missing value for $1"
fi
file="$2"
shift 2
;;
-h | --help)
usage_archive_extract
exit 0
;;
--)
shift
while [[ $# -gt 0 ]]; do
shift
done
;;
-*)
usage_archive_extract
error "Unknown option: $1"
;;
*)
shift
;;
esac
done
ensure_tools "$compression"
local -a tar_opts=(-x -f "$file" -C "$directory")
case "$compression" in
gzip)
tar_opts+=(-z)
;;
zstd)
tar_opts+=(--zstd)
;;
none) ;;
*)
error "Unsupported compression algorithm: $compression"
;;
esac
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
if [[ -n $path ]]; then
tar_opts+=(--exclude "$path")
fi
done
# Ensure destination directory exists.
mkdir -p "$directory" || error "Failed to create directory: $directory"
log "Extracting archive:"
log " Compression: $compression"
log " Directory: $directory"
log " Archive: $file"
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
umask 077
tar "${tar_opts[@]}" "${paths[@]}"
printf 'Extracted %s into %s\n' "$file" "$directory"
}
archive_wait_and_extract() {
load_defaults
local timeout="${1:-300}"
local quiet="${2:-}"
local file="${DEFAULT_ARCHIVE_PATH}"
local start now
start=$(date +%s)
while true; do
if [[ -f "$file" ]]; then
archive_extract -f "$file"
return 0
fi
if ((timeout <= 0)); then
break
fi
now=$(date +%s)
if ((now - start >= timeout)); then
break
fi
sleep 5
done
if [[ -z $quiet ]]; then
printf 'ERROR: Timed out waiting for archive: %s\n' "$file" >&2
fi
return 2
}
+3 -3
View File
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -47,7 +47,7 @@ module "coder-login" {
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -103,7 +103,7 @@ EOF
```tf
module "auggie" {
source = "registry.coder.com/coder-labs/auggie/coder"
version = "0.1.0"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
+4 -2
View File
@@ -66,7 +66,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.6.0"
default = "v0.10.0"
validation {
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+", var.agentapi_version))
error_message = "agentapi_version must be a valid semantic version starting with 'v', like 'v0.3.3'."
@@ -174,13 +174,15 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".auggie-module"
folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
+10 -9
View File
@@ -13,10 +13,10 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "3.0.0"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
folder = "/home/coder/project"
workdir = "/home/coder/project"
}
```
@@ -33,10 +33,11 @@ module "codex" {
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "3.0.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
folder = "/home/coder/project"
workdir = "/home/coder/project"
report_tasks = false
}
```
@@ -60,11 +61,11 @@ module "coder-login" {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "3.0.0"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_parameter.ai_prompt.value
folder = "/home/coder/project"
workdir = "/home/coder/project"
# Custom configuration for full auto mode
base_config_toml = <<-EOT
@@ -75,7 +76,7 @@ module "codex" {
```
> [!WARNING]
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
## How it Works
@@ -106,7 +107,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "2.0.0"
version = "3.0.0"
# ... other variables ...
# Override default configuration
@@ -137,7 +138,7 @@ module "codex" {
> [!IMPORTANT]
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
> The module automatically configures Codex with your API key and model preferences.
> folder is a required variable for the module to function correctly.
> workdir is a required variable for the module to function correctly.
## References
@@ -47,7 +47,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
install_codex: props?.skipCodexMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
codex_model: "gpt-4-turbo",
folder: "/home/coder",
workdir: "/home/coder",
...props?.moduleVariables,
},
registerCleanup,
@@ -166,12 +166,12 @@ describe("codex", async () => {
expect(postInstallLog).toContain("post-install-script");
});
test("folder-variable", async () => {
const folder = "/tmp/codex-test-folder";
test("workdir-variable", async () => {
const workdir = "/tmp/codex-test-workdir";
const { id } = await setup({
skipCodexMock: false,
moduleVariables: {
folder,
workdir,
},
});
await execModuleScript(id);
@@ -179,7 +179,7 @@ describe("codex", async () => {
id,
"/home/coder/.codex-module/install.log",
);
expect(resp).toContain(folder);
expect(resp).toContain(workdir);
});
test("additional-mcp-servers", async () => {
+45 -8
View File
@@ -36,11 +36,41 @@ variable "icon" {
default = "/icon/openai.svg"
}
variable "folder" {
variable "workdir" {
type = string
description = "The folder to run Codex in."
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Codex"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Codex"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Codex CLI"
}
variable "install_codex" {
type = bool
description = "Whether to install Codex."
@@ -80,7 +110,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
default = "v0.10.0"
}
variable "codex_model" {
@@ -120,6 +150,7 @@ resource "coder_env" "openai_api_key" {
}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "codex"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
@@ -128,18 +159,21 @@ locals {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
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 = "Codex"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Codex CLI"
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_subdomain = var.subdomain
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
@@ -151,8 +185,9 @@ module "agentapi" {
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_CODEX_MODEL='${var.codex_model}' \
ARG_CODEX_START_DIRECTORY='${var.folder}' \
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
/tmp/start.sh
EOT
@@ -164,12 +199,14 @@ module "agentapi" {
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_INSTALL='${var.install_codex}' \
ARG_CODEX_VERSION='${var.codex_version}' \
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODEX_START_DIRECTORY='${var.folder}' \
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
@@ -22,6 +22,8 @@ printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
echo "======================================"
set +o nounset
@@ -100,13 +102,20 @@ EOF
append_mcp_servers_section() {
local config_path="$1"
if [ "${ARG_REPORT_TASKS}" == "false" ]; then
ARG_CODER_MCP_APP_STATUS_SLUG=""
CODER_MCP_AI_AGENTAPI_URL=""
else
CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
fi
cat << EOF >> "$config_path"
# MCP Servers Configuration
[mcp_servers.Coder]
command = "coder"
args = ["exp", "mcp", "server"]
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
type = "stdio"
@@ -159,7 +168,21 @@ function add_instruction_prompt_if_exists() {
fi
}
function add_auth_json() {
AUTH_JSON_PATH="$HOME/.codex/auth.json"
mkdir -p "$(dirname "$AUTH_JSON_PATH")"
AUTH_JSON=$(
cat << EOF
{
"OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}"
}
EOF
)
echo "$AUTH_JSON" > "$AUTH_JSON_PATH"
}
install_codex
codex --version
populate_config_toml
add_instruction_prompt_if_exists
add_auth_json
@@ -22,6 +22,7 @@ printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided"
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
echo "======================================"
set +o nounset
CODEX_ARGS=()
@@ -57,7 +58,11 @@ fi
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT"
else
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
fi
CODEX_ARGS+=("$PROMPT")
else
printf "No task prompt given.\n"
@@ -0,0 +1,210 @@
---
display_name: Copilot CLI
description: GitHub Copilot CLI agent for AI-powered terminal assistance
icon: ../../../../.icons/github.svg
verified: false
tags: [agent, copilot, ai, github, tasks]
---
# Copilot
Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-copilot-cli) in your workspace for AI-powered coding assistance directly from the terminal. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
}
```
> [!IMPORTANT]
> This example assumes you have [Coder external authentication](https://coder.com/docs/admin/external-auth) configured with `id = "github"`. If not, you can provide a direct token using the `github_token` variable or provide the correct external authentication id for GitHub by setting `external_auth_id = "my-github"`.
> [!NOTE]
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
## Prerequisites
- **Node.js v22+** and **npm v10+**
- **[Active Copilot subscription](https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot)** (GitHub Copilot Pro, Pro+, Business, or Enterprise)
- **GitHub authentication** via one of:
- [Coder external authentication](https://coder.com/docs/admin/external-auth) (recommended)
- Direct token via `github_token` variable
- Interactive login in Copilot
## Examples
### Usage with Tasks
For development environments where you want Copilot to have full access to tools and automatically resume sessions:
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Initial task prompt for Copilot."
mutable = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
ai_prompt = data.coder_parameter.ai_prompt.value
copilot_model = "claude-sonnet-4.5"
allow_all_tools = true
resume_session = true
trusted_directories = ["/home/coder/projects", "/tmp"]
}
```
### Advanced Configuration
Customize tool permissions, MCP servers, and Copilot settings:
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
# Version pinning (defaults to "latest", use specific version if desired)
copilot_version = "0.0.334"
# Tool permissions
allow_tools = ["shell(git)", "shell(npm)", "write"]
trusted_directories = ["/home/coder/projects", "/tmp"]
# Custom Copilot configuration
copilot_config = jsonencode({
banner = "never"
theme = "dark"
})
# MCP server configuration
mcp_config = jsonencode({
mcpServers = {
filesystem = {
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/coder/projects"]
description = "Provides file system access to the workspace"
name = "Filesystem"
timeout = 3000
type = "local"
tools = ["*"]
trust = true
}
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
description = "Browser automation for testing and previewing changes"
name = "Playwright"
timeout = 5000
type = "local"
tools = ["*"]
trust = false
}
}
})
# Pre-install Node.js if needed
pre_install_script = <<-EOT
#!/bin/bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
EOT
}
```
> [!NOTE]
> GitHub Copilot CLI does not automatically install MCP servers. You have two options:
>
> - Use `npx -y` in the MCP config (shown above) to auto-install on each run
> - Pre-install MCP servers in `pre_install_script` for faster startup (e.g., `npm install -g @modelcontextprotocol/server-filesystem`)
### Direct Token Authentication
Use this example when you want to provide a GitHub Personal Access Token instead of using Coder external auth:
```tf
variable "github_token" {
type = string
description = "GitHub Personal Access Token"
sensitive = true
}
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/projects"
github_token = var.github_token
}
```
### Standalone Mode
Run Copilot as a command-line tool without task reporting or web interface. This installs and configures Copilot, making it available as a CLI app in the Coder agent bar that you can launch to interact with Copilot directly from your terminal. Set `report_tasks = false` to disable integration with Coder Tasks.
```tf
module "copilot" {
source = "registry.coder.com/coder-labs/copilot/coder"
version = "0.2.2"
agent_id = coder_agent.example.id
workdir = "/home/coder"
report_tasks = false
cli_app = true
}
```
## Authentication
The module supports multiple authentication methods (in priority order):
1. **[Coder External Auth](https://coder.com/docs/admin/external-auth) (Recommended)** - Automatic if GitHub external auth is configured in Coder
2. **Direct Token** - Pass `github_token` variable (OAuth or Personal Access Token)
3. **Interactive** - Copilot prompts for login via `/login` command if no auth found
> [!NOTE]
> OAuth tokens work best with Copilot. Personal Access Tokens may have limited functionality.
## Session Resumption
By default, the module resumes the latest Copilot session when the workspace restarts. Set `resume_session = false` to always start fresh sessions.
> [!NOTE]
> Session resumption requires persistent storage for the home directory or workspace volume. Without persistent storage, sessions will not resume across workspace restarts.
## Troubleshooting
If you encounter any issues, check the log files in the `~/.copilot-module` directory within your workspace for detailed information.
```bash
# Installation logs
cat ~/.copilot-module/install.log
# Startup logs
cat ~/.copilot-module/agentapi-start.log
# Pre/post install script logs
cat ~/.copilot-module/pre_install.log
cat ~/.copilot-module/post_install.log
```
> [!NOTE]
> To use tasks with Copilot, you must have an active GitHub Copilot subscription.
> The `workdir` variable is required and specifies the directory where Copilot will run.
## References
- [GitHub Copilot CLI Documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
- [Installing GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
@@ -0,0 +1,236 @@
run "defaults_are_correct" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = var.copilot_model == "claude-sonnet-4.5"
error_message = "Default model should be 'claude-sonnet-4.5'"
}
assert {
condition = var.report_tasks == true
error_message = "Task reporting should be enabled by default"
}
assert {
condition = var.resume_session == true
error_message = "Session resumption should be enabled by default"
}
assert {
condition = var.allow_all_tools == false
error_message = "allow_all_tools should be disabled by default"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug env var should be created"
}
assert {
condition = resource.coder_env.mcp_app_status_slug.value == "copilot"
error_message = "Status slug value should be 'copilot'"
}
}
run "github_token_creates_env_var" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = "test_github_token_abc123"
}
assert {
condition = length(resource.coder_env.github_token) == 1
error_message = "github_token env var should be created when token is provided"
}
assert {
condition = resource.coder_env.github_token[0].name == "GITHUB_TOKEN"
error_message = "github_token env var name should be 'GITHUB_TOKEN'"
}
assert {
condition = resource.coder_env.github_token[0].value == "test_github_token_abc123"
error_message = "github_token env var value should match input"
}
}
run "github_token_not_created_when_empty" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
github_token = ""
}
assert {
condition = length(resource.coder_env.github_token) == 0
error_message = "github_token env var should not be created when empty"
}
}
run "copilot_model_env_var_for_non_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4"
}
assert {
condition = length(resource.coder_env.copilot_model) == 1
error_message = "copilot_model env var should be created for non-default model"
}
assert {
condition = resource.coder_env.copilot_model[0].name == "COPILOT_MODEL"
error_message = "copilot_model env var name should be 'COPILOT_MODEL'"
}
assert {
condition = resource.coder_env.copilot_model[0].value == "claude-sonnet-4"
error_message = "copilot_model env var value should match input"
}
}
run "copilot_model_not_created_for_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "claude-sonnet-4.5"
}
assert {
condition = length(resource.coder_env.copilot_model) == 0
error_message = "copilot_model env var should not be created for default model"
}
}
run "model_validation_accepts_valid_models" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_model = "gpt-5"
}
assert {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "Model should be one of the valid options"
}
}
run "copilot_config_merges_with_trusted_directories" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = length(local.final_copilot_config) > 0
error_message = "final_copilot_config should be computed"
}
# Verify workdir is trimmed of trailing slash
assert {
condition = local.workdir == "/home/coder/project"
error_message = "workdir should be trimmed of trailing slash"
}
}
run "custom_copilot_config_overrides_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
})
}
assert {
condition = var.copilot_config != ""
error_message = "Custom copilot config should be set"
}
assert {
condition = jsondecode(local.final_copilot_config).banner == "always"
error_message = "Custom banner setting should be applied"
}
assert {
condition = jsondecode(local.final_copilot_config).theme == "dark"
error_message = "Custom theme setting should be applied"
}
}
run "trusted_directories_merged_with_custom_config" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder/project"
copilot_config = jsonencode({
banner = "always"
theme = "dark"
trusted_folders = ["/custom"]
})
trusted_directories = ["/workspace", "/data"]
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/custom")
error_message = "Custom trusted folder should be included"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/home/coder/project")
error_message = "Workdir should be included in trusted folders"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/workspace")
error_message = "trusted_directories should be merged into config"
}
assert {
condition = contains(jsondecode(local.final_copilot_config).trusted_folders, "/data")
error_message = "All trusted_directories should be merged into config"
}
}
run "app_slug_is_consistent" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = local.app_slug == "copilot"
error_message = "app_slug should be 'copilot'"
}
assert {
condition = local.module_dir_name == ".copilot-module"
error_message = "module_dir_name should be '.copilot-module'"
}
}
@@ -0,0 +1,136 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("copilot", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
it("creates mcp_app_status_slug env var", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"mcp_app_status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("copilot");
});
it("creates github_token env var with correct value", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "test_token_12345",
});
const githubTokenEnv = findResourceInstance(
state,
"coder_env",
"github_token",
);
expect(githubTokenEnv).toBeDefined();
expect(githubTokenEnv.name).toBe("GITHUB_TOKEN");
expect(githubTokenEnv.value).toBe("test_token_12345");
});
it("does not create github_token env var when empty", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
github_token: "",
});
const githubTokenEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "github_token",
);
expect(githubTokenEnvs.length).toBe(0);
});
it("creates copilot_model env var for non-default models", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4",
});
const modelEnv = findResourceInstance(state, "coder_env", "copilot_model");
expect(modelEnv).toBeDefined();
expect(modelEnv.name).toBe("COPILOT_MODEL");
expect(modelEnv.value).toBe("claude-sonnet-4");
});
it("does not create copilot_model env var for default model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
});
const modelEnvs = state.resources.filter(
(r) => r.type === "coder_env" && r.name === "copilot_model",
);
expect(modelEnvs.length).toBe(0);
});
it("creates coder_script resources via agentapi module", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
});
// The agentapi module should create coder_script resources for install and start
const scripts = state.resources.filter((r) => r.type === "coder_script");
expect(scripts.length).toBeGreaterThan(0);
});
it("validates copilot_model accepts valid values", async () => {
// Test valid models don't throw errors
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "gpt-5",
}),
).resolves.toBeDefined();
await expect(
runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder",
copilot_model: "claude-sonnet-4.5",
}),
).resolves.toBeDefined();
});
it("merges trusted_directories with custom copilot_config", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
workdir: "/home/coder/project",
trusted_directories: JSON.stringify(["/workspace", "/data"]),
copilot_config: JSON.stringify({
banner: "always",
theme: "dark",
trusted_folders: ["/custom"],
}),
});
// Verify that the state was created successfully with the merged config
// The actual merging logic is tested in the .tftest.hcl file
expect(state).toBeDefined();
expect(state.resources).toBeDefined();
});
});
+302
View File
@@ -0,0 +1,302 @@
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."
}
variable "workdir" {
type = string
description = "The folder to run Copilot in."
}
variable "external_auth_id" {
type = string
description = "ID of the GitHub external auth provider configured in Coder."
default = "github"
}
variable "github_token" {
type = string
description = "GitHub OAuth token or Personal Access Token. If provided, this will be used instead of auto-detecting authentication."
default = ""
sensitive = true
}
variable "copilot_model" {
type = string
description = "Model to use. Supported values: claude-sonnet-4, claude-sonnet-4.5 (default), gpt-5."
default = "claude-sonnet-4.5"
validation {
condition = contains(["claude-sonnet-4", "claude-sonnet-4.5", "gpt-5"], var.copilot_model)
error_message = "copilot_model must be one of: claude-sonnet-4, claude-sonnet-4.5, gpt-5."
}
}
variable "copilot_config" {
type = string
description = "Custom Copilot configuration as JSON string. Leave empty to use default configuration with banner disabled, theme set to auto, and workdir as trusted folder."
default = ""
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for programmatic mode."
default = ""
}
variable "system_prompt" {
type = string
description = "The system prompt to use for the Copilot server. Task reporting instructions are automatically added when report_tasks is enabled."
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
}
variable "trusted_directories" {
type = list(string)
description = "Additional directories to trust for Copilot operations."
default = []
}
variable "allow_all_tools" {
type = bool
description = "Allow all tools without prompting (equivalent to --allow-all-tools)."
default = false
}
variable "allow_tools" {
type = list(string)
description = "Specific tools to allow: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "deny_tools" {
type = list(string)
description = "Specific tools to deny: shell(command), write, or MCP_SERVER_NAME."
default = []
}
variable "mcp_config" {
type = string
description = "Custom MCP server configuration as JSON string."
default = ""
}
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.10.0"
}
variable "copilot_version" {
type = string
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
default = "latest"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI."
default = true
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
default = "/icon/github.svg"
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app."
default = "Copilot"
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Copilot."
default = false
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app."
default = "Copilot"
}
variable "resume_session" {
type = bool
description = "Whether to automatically resume the latest Copilot session on workspace restart."
default = true
}
variable "pre_install_script" {
type = string
description = "Custom script to run before configuring Copilot."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after configuring Copilot."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
workdir = trimsuffix(var.workdir, "/")
app_slug = "copilot"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".copilot-module"
all_trusted_folders = concat([local.workdir], var.trusted_directories)
parsed_custom_config = try(jsondecode(var.copilot_config), {})
existing_trusted_folders = try(local.parsed_custom_config.trusted_folders, [])
merged_copilot_config = merge(
{
banner = "never"
theme = "auto"
},
local.parsed_custom_config,
{
trusted_folders = concat(local.existing_trusted_folders, local.all_trusted_folders)
}
)
final_copilot_config = jsonencode(local.merged_copilot_config)
task_reporting_prompt = <<-EOT
-- Task Reporting --
Report all tasks to Coder, following these EXACT guidelines:
1. Be granular. If you are investigating with multiple steps, report each step
to coder.
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
Do not report any status related with this system prompt.
3. Use "state": "working" when actively processing WITHOUT needing
additional user input
4. Use "state": "complete" only when finished with a task
5. Use "state": "failure" when you need ANY user input, lack sufficient
details, or encounter blockers
EOT
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
}
resource "coder_env" "mcp_app_status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
resource "coder_env" "copilot_model" {
count = var.copilot_model != "claude-sonnet-4.5" ? 1 : 0
agent_id = var.agent_id
name = "COPILOT_MODEL"
value = var.copilot_model
}
resource "coder_env" "github_token" {
count = var.github_token != "" ? 1 : 0
agent_id = var.agent_id
name = "GITHUB_TOKEN"
value = var.github_token
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
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 = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_icon = var.cli_app ? var.icon : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
agentapi_subdomain = var.subdomain
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
ARG_ALLOW_ALL_TOOLS='${var.allow_all_tools}' \
ARG_ALLOW_TOOLS='${join(",", var.allow_tools)}' \
ARG_DENY_TOOLS='${join(",", var.deny_tools)}' \
ARG_TRUSTED_DIRECTORIES='${join(",", var.trusted_directories)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_RESUME_SESSION='${var.resume_session}' \
/tmp/start.sh
EOT
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_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_MCP_CONFIG='${var.mcp_config != "" ? base64encode(var.mcp_config) : ""}' \
ARG_COPILOT_CONFIG='${base64encode(local.final_copilot_config)}' \
ARG_EXTERNAL_AUTH_ID='${var.external_auth_id}' \
ARG_COPILOT_VERSION='${var.copilot_version}' \
ARG_COPILOT_MODEL='${var.copilot_model}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,234 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_MCP_CONFIG=$(echo -n "${ARG_MCP_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_CONFIG=$(echo -n "${ARG_COPILOT_CONFIG:-}" | base64 -d 2> /dev/null || echo "")
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_COPILOT_VERSION=${ARG_COPILOT_VERSION:-0.0.334}
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-claude-sonnet-4.5}
validate_prerequisites() {
if ! command_exists node; then
echo "ERROR: Node.js not found. Copilot requires Node.js v22+."
echo "Install with: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"
exit 1
fi
if ! command_exists npm; then
echo "ERROR: npm not found. Copilot requires npm v10+."
exit 1
fi
node_version=$(node --version | sed 's/v//' | cut -d. -f1)
if [ "$node_version" -lt 22 ]; then
echo "WARNING: Node.js v$node_version detected. Copilot requires v22+."
fi
}
install_copilot() {
if ! command_exists copilot; then
echo "Installing GitHub Copilot CLI (version: ${ARG_COPILOT_VERSION})..."
if [ "$ARG_COPILOT_VERSION" = "latest" ]; then
npm install -g @github/copilot
else
npm install -g "@github/copilot@${ARG_COPILOT_VERSION}"
fi
if ! command_exists copilot; then
echo "ERROR: Failed to install Copilot"
exit 1
fi
echo "GitHub Copilot CLI installed successfully"
else
echo "GitHub Copilot CLI already installed"
fi
}
check_github_authentication() {
echo "Checking GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
echo "✓ GitHub token provided via module configuration"
return 0
fi
if command_exists coder; then
if coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via Coder external auth"
return 0
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ GitHub OAuth authentication via GitHub CLI"
return 0
fi
echo "⚠ No GitHub authentication detected"
echo " Copilot will prompt for authentication when started"
echo " For seamless experience, configure GitHub external auth in Coder or run 'gh auth login'"
return 0
}
setup_copilot_configurations() {
mkdir -p "$ARG_WORKDIR"
local module_path="$HOME/.copilot-module"
mkdir -p "$module_path"
setup_copilot_config
echo "$ARG_WORKDIR" > "$module_path/trusted_directories"
}
setup_copilot_config() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
local copilot_config_dir="$XDG_CONFIG_HOME/.copilot"
local copilot_config_file="$copilot_config_dir/config.json"
local mcp_config_file="$copilot_config_dir/mcp-config.json"
mkdir -p "$copilot_config_dir"
if [ -n "$ARG_COPILOT_CONFIG" ]; then
echo "Setting up Copilot configuration..."
if command_exists jq; then
echo "$ARG_COPILOT_CONFIG" | jq 'del(.mcpServers)' > "$copilot_config_file"
else
echo "$ARG_COPILOT_CONFIG" > "$copilot_config_file"
fi
echo "Setting up MCP server configuration..."
setup_mcp_config "$mcp_config_file"
else
echo "ERROR: No Copilot configuration provided"
exit 1
fi
}
setup_mcp_config() {
local mcp_config_file="$1"
echo '{"mcpServers": {}}' > "$mcp_config_file"
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Adding Coder MCP server for task reporting..."
setup_coder_mcp_server "$mcp_config_file"
fi
if [ -n "$ARG_MCP_CONFIG" ]; then
echo "Adding custom MCP servers..."
add_custom_mcp_servers "$mcp_config_file"
fi
echo "MCP configuration completed: $mcp_config_file"
}
setup_coder_mcp_server() {
local mcp_config_file="$1"
local coder_mcp_wrapper_script
coder_mcp_wrapper_script=$(
cat << EOF
#!/usr/bin/env bash
set -e
export CODER_MCP_APP_STATUS_SLUG="${ARG_MCP_APP_STATUS_SLUG}"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
export CODER_AGENT_URL="${CODER_AGENT_URL}"
export CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN}"
exec coder exp mcp server
EOF
)
echo "$coder_mcp_wrapper_script" > "/tmp/coder-mcp-server.sh"
chmod +x /tmp/coder-mcp-server.sh
local coder_mcp_config
coder_mcp_config=$(
cat << EOF
{
"mcpServers": {
"coder": {
"command": "/tmp/coder-mcp-server.sh",
"args": [],
"description": "CRITICAL: Call this tool immediately after receiving ANY user message to report task status. Rules: (1) Call FIRST before doing work - report what you will do with state='working'. (2) Be granular - report each step separately. (3) State 'working' = actively processing without needing user input. (4) State 'complete' = task 100% finished. (5) State 'failure' = need user input, missing info, or blocked. Example: User says 'fix the bug' -> call with state='working', description='Investigating authentication bug'. When done -> call with state='complete', description='Fixed token validation'. You MUST report on every interaction.",
"name": "Coder",
"timeout": 3000,
"type": "local",
"tools": ["*"],
"trust": true
}
}
}
EOF
)
echo "$coder_mcp_config" > "$mcp_config_file"
}
add_custom_mcp_servers() {
local mcp_config_file="$1"
if command_exists jq; then
local custom_servers
custom_servers=$(echo "$ARG_MCP_CONFIG" | jq '.mcpServers // {}')
local updated_config
updated_config=$(jq --argjson custom "$custom_servers" '.mcpServers += $custom' "$mcp_config_file")
echo "$updated_config" > "$mcp_config_file"
elif command_exists node; then
node -e "
const fs = require('fs');
const existing = JSON.parse(fs.readFileSync('$mcp_config_file', 'utf8'));
const input = JSON.parse(\`$ARG_MCP_CONFIG\`);
const custom = input.mcpServers || {};
existing.mcpServers = {...existing.mcpServers, ...custom};
fs.writeFileSync('$mcp_config_file', JSON.stringify(existing, null, 2));
"
else
echo "WARNING: jq and node not available, cannot merge custom MCP servers"
fi
}
configure_copilot_model() {
if [ -n "$ARG_COPILOT_MODEL" ] && [ "$ARG_COPILOT_MODEL" != "claude-sonnet-4.5" ]; then
echo "Setting Copilot model to: $ARG_COPILOT_MODEL"
copilot config model "$ARG_COPILOT_MODEL" || {
echo "WARNING: Failed to set model via copilot config, will use environment variable fallback"
export COPILOT_MODEL="$ARG_COPILOT_MODEL"
}
fi
}
configure_coder_integration() {
if [ "$ARG_REPORT_TASKS" = "true" ] && [ -n "$ARG_MCP_APP_STATUS_SLUG" ]; then
echo "Configuring Copilot task reporting..."
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
echo "✓ Coder MCP server configured for task reporting"
else
echo "Task reporting disabled or no app status slug provided."
export CODER_MCP_APP_STATUS_SLUG=""
export CODER_MCP_AI_AGENTAPI_URL=""
fi
}
validate_prerequisites
install_copilot
check_github_authentication
setup_copilot_configurations
configure_copilot_model
configure_coder_integration
echo "Copilot module setup completed."
@@ -0,0 +1,157 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_COPILOT_MODEL=${ARG_COPILOT_MODEL:-}
ARG_ALLOW_ALL_TOOLS=${ARG_ALLOW_ALL_TOOLS:-false}
ARG_ALLOW_TOOLS=${ARG_ALLOW_TOOLS:-}
ARG_DENY_TOOLS=${ARG_DENY_TOOLS:-}
ARG_TRUSTED_DIRECTORIES=${ARG_TRUSTED_DIRECTORIES:-}
ARG_EXTERNAL_AUTH_ID=${ARG_EXTERNAL_AUTH_ID:-github}
ARG_RESUME_SESSION=${ARG_RESUME_SESSION:-true}
validate_copilot_installation() {
if ! command_exists copilot; then
echo "ERROR: Copilot not installed. Run: npm install -g @github/copilot"
exit 1
fi
}
build_initial_prompt() {
local initial_prompt=""
if [ -n "$ARG_AI_PROMPT" ]; then
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
initial_prompt="$ARG_SYSTEM_PROMPT
$ARG_AI_PROMPT"
else
initial_prompt="$ARG_AI_PROMPT"
fi
fi
echo "$initial_prompt"
}
build_copilot_args() {
COPILOT_ARGS=()
if [ "$ARG_ALLOW_ALL_TOOLS" = "true" ]; then
COPILOT_ARGS+=(--allow-all-tools)
fi
if [ -n "$ARG_ALLOW_TOOLS" ]; then
IFS=',' read -ra ALLOW_ARRAY <<< "$ARG_ALLOW_TOOLS"
for tool in "${ALLOW_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--allow-tool "$tool")
fi
done
fi
if [ -n "$ARG_DENY_TOOLS" ]; then
IFS=',' read -ra DENY_ARRAY <<< "$ARG_DENY_TOOLS"
for tool in "${DENY_ARRAY[@]}"; do
if [ -n "$tool" ]; then
COPILOT_ARGS+=(--deny-tool "$tool")
fi
done
fi
}
check_existing_session() {
if [ "$ARG_RESUME_SESSION" = "true" ]; then
if copilot --help > /dev/null 2>&1; then
local session_dir="$HOME/.copilot/history-session-state"
if [ -d "$session_dir" ] && [ -n "$(ls "$session_dir"/session_*_*.json 2> /dev/null)" ]; then
echo "Found existing Copilot session. Will continue latest session." >&2
return 0
fi
fi
fi
return 1
}
setup_github_authentication() {
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
echo "Setting up GitHub authentication..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
export GH_TOKEN="$GITHUB_TOKEN"
echo "✓ Using GitHub token from module configuration"
return 0
fi
if command_exists coder; then
local github_token
if github_token=$(coder external-auth access-token "${ARG_EXTERNAL_AUTH_ID:-github}" 2> /dev/null); then
if [ -n "$github_token" ] && [ "$github_token" != "null" ]; then
export GITHUB_TOKEN="$github_token"
export GH_TOKEN="$github_token"
echo "✓ Using Coder external auth OAuth token"
return 0
fi
fi
fi
if command_exists gh && gh auth status > /dev/null 2>&1; then
echo "✓ Using GitHub CLI OAuth authentication"
return 0
fi
echo "⚠ No GitHub authentication available"
echo " Copilot will prompt for login during first use"
echo " Use the '/login' command in Copilot to authenticate"
return 0
}
start_agentapi() {
echo "Starting in directory: $ARG_WORKDIR"
cd "$ARG_WORKDIR"
build_copilot_args
if check_existing_session; then
echo "Continuing latest Copilot session..."
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot --continue
fi
else
echo "Starting new Copilot session..."
local initial_prompt
initial_prompt=$(build_initial_prompt)
if [ -n "$initial_prompt" ]; then
echo "Using initial prompt with system context"
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server -I="$initial_prompt" --type copilot --term-width 120 --term-height 40 -- copilot
fi
else
if [ ${#COPILOT_ARGS[@]} -gt 0 ]; then
echo "Copilot arguments: ${COPILOT_ARGS[*]}"
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot "${COPILOT_ARGS[@]}"
else
agentapi server --type copilot --term-width 120 --term-height 40 -- copilot
fi
fi
fi
}
setup_github_authentication
validate_copilot_installation
start_agentapi
@@ -0,0 +1,12 @@
#!/bin/bash
set -euo pipefail
if [[ "$1" == "--version" ]]; then
echo "GitHub Copilot CLI v1.0.0"
exit 0
fi
while true; do
echo "$(date) - Copilot mock running..."
sleep 15
done
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
```tf
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -42,7 +42,7 @@ module "coder-login" {
module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.1"
version = "0.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
@@ -56,7 +56,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.5.0"
default = "v0.10.0"
}
variable "force" {
@@ -113,6 +113,7 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".cursor-cli-module"
folder = trimsuffix(var.folder, "/")
}
# Expose status slug and API key to the agent environment
@@ -131,9 +132,10 @@ resource "coder_env" "cursor_api_key" {
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
+4 -4
View File
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
module "gemini" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
gemini_model = "gemini-2.5-flash"
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
```tf
module "gemini" {
source = "registry.coder.com/coder-labs/gemini/coder"
version = "2.0.0"
version = "2.1.1"
agent_id = coder_agent.example.id
gemini_api_key = var.gemini_api_key
folder = "/home/coder/project"
+4 -2
View File
@@ -81,7 +81,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.2.3"
default = "v0.10.0"
}
variable "gemini_model" {
@@ -172,13 +172,15 @@ EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".gemini-module"
folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.folder
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
@@ -0,0 +1,22 @@
---
display_name: Nextflow
description: A module that adds Nextflow to your Coder template.
icon: ../../../../.icons/nextflow.svg
verified: true
tags: [nextflow, workflow, hpc, bioinformatics]
---
# Nextflow
A module that adds Nextflow to your Coder template.
![Nextflow](../../.images/nextflow.png)
```tf
module "nextflow" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/nextflow/coder"
version = "0.9.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,106 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "nextflow_version" {
type = string
description = "Nextflow version"
default = "25.04.7"
}
variable "project_path" {
type = string
description = "The path to Nextflow project, it will be mounted in the container."
}
variable "http_server_port" {
type = number
description = "The port to run HTTP server on."
default = 9876
}
variable "http_server_reports_dir" {
type = string
description = "Subdirectory for HTTP server reports, relative to the project path."
default = "reports"
}
variable "http_server_log_path" {
type = string
description = "HTTP server logs"
default = "/tmp/nextflow_reports.log"
}
variable "stub_run" {
type = bool
description = "Execute a stub run?"
default = false
}
variable "stub_run_command" {
type = string
description = "Nextflow command to be executed in the stub run."
default = "run rnaseq-nf -with-report reports/report.html -with-trace reports/trace.txt -with-timeline reports/timeline.html -with-dag reports/flowchart.png"
}
variable "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 "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "nextflow" {
agent_id = var.agent_id
display_name = "nextflow"
icon = "/icon/nextflow.svg"
script = templatefile("${path.module}/run.sh", {
NEXTFLOW_VERSION : var.nextflow_version,
PROJECT_PATH : var.project_path,
HTTP_SERVER_PORT : var.http_server_port,
HTTP_SERVER_REPORTS_DIR : var.http_server_reports_dir,
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
STUB_RUN : var.stub_run,
STUB_RUN_COMMAND : var.stub_run_command,
})
run_on_start = true
}
resource "coder_app" "nextflow" {
agent_id = var.agent_id
slug = "nextflow-reports"
display_name = "Nextflow Reports"
url = "http://localhost:${var.http_server_port}"
icon = "/icon/nextflow.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,49 @@
#!/usr/bin/env sh
set -eu
BOLD='\033[0;1m'
RESET='\033[0m'
printf "$${BOLD}Starting Nextflow...$${RESET}\n"
if ! command -v nextflow > /dev/null 2>&1; then
# Update system dependencies
sudo apt update
sudo apt install openjdk-21-jdk graphviz salmon fastqc multiqc -y
# Install nextflow
export NXF_VER=${NEXTFLOW_VERSION}
curl -s https://get.nextflow.io | bash
sudo mv nextflow /usr/local/bin/
sudo chmod +x /usr/local/bin/nextflow
# Verify installation
tmp_verify=$(mktemp -d coder-nextflow-XXXXXX)
nextflow run hello \
-with-report "$${tmp_verify}/report.html" \
-with-trace "$${tmp_verify}/trace.txt" \
-with-timeline "$${tmp_verify}/timeline.html" \
-with-dag "$${tmp_verify}/flowchart.png"
rm -r "$${tmp_verify}"
else
echo "Nextflow is already installed\n\n"
fi
if [ ! -z ${PROJECT_PATH} ]; then
# Project is located at PROJECT_PATH
echo "Change directory: ${PROJECT_PATH}"
cd ${PROJECT_PATH}
fi
# Start a web server to preview reports
mkdir -p ${HTTP_SERVER_REPORTS_DIR}
echo "Starting HTTP server in background, check logs: ${HTTP_SERVER_LOG_PATH}"
python3 -m http.server --directory ${HTTP_SERVER_REPORTS_DIR} ${HTTP_SERVER_PORT} > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
# Stub run?
if [ "${STUB_RUN}" = "true" ]; then
nextflow ${STUB_RUN_COMMAND} -stub-run
fi
printf "\n$${BOLD}Nextflow ${NEXTFLOW_VERSION} is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}$${RESET}\n"
@@ -1,8 +1,8 @@
---
display_name: Amp CLI
display_name: Amp
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: false
verified: true
tags: [agent, sourcegraph, amp, ai, tasks]
---
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "2.0.0"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -23,8 +23,10 @@ module "amp-cli" {
## Prerequisites
- Include the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module in your template
- Node.js and npm are automatically installed (via NVM) if not already available
- **Default (official installer)**: No prerequisites - the official installer includes its own runtime (Bun)
- **npm installation (`install_via_npm = true`)**: Requires Node.js and npm to be installed before Amp installation
- Required for Alpine Linux or other musl-based systems
- Ensure Node.js and npm are available in your workspace image or via earlier provisioning steps
## Usage Example
@@ -35,52 +37,55 @@ data "coder_parameter" "ai_prompt" {
type = "string"
default = ""
mutable = true
}
# Set system prompt for Amp CLI via environment variables
resource "coder_agent" "main" {
# ...
env = {
SOURCEGRAPH_AMP_SYSTEM_PROMPT = <<-EOT
You are an Amp assistant that helps developers debug and write code efficiently.
Always log task status to Coder.
EOT
SOURCEGRAPH_AMP_TASK_PROMPT = data.coder_parameter.ai_prompt.value
}
}
variable "sourcegraph_amp_api_key" {
variable "amp_api_key" {
type = string
description = "Sourcegraph Amp API key. Get one at https://ampcode.com/settings"
sensitive = true
}
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
amp_version = "2.0.0"
agent_id = coder_agent.example.id
amp_api_key = var.amp_api_key # recommended for tasks usage
workdir = "/home/coder/project"
instruction_prompt = <<-EOT
# Instructions
- Start every response with `amp > `
EOT
ai_prompt = data.coder_parameter.ai_prompt.value
base_amp_config = jsonencode({
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
"amp.tools.stopTimeout" = 600
"amp.git.commit.ampThread.enabled" = true
"amp.git.commit.coauthor.enabled" = true
"amp.terminal.commands.nodeSpawn.loadProfile" = "daily"
"amp.permissions" = [
{ "tool" : "mcp__coder__*", "action" : "allow" },
{ "tool" : "Bash", "action" : "allow", "context" : "thread" },
{ "tool" : "Bash", "matches" : { "cmd" : ["rm -rf /*", "rm -rf ~/*"] }, "action" : "reject", "context" : "subagent" },
{ "tool" : "edit_file", "action" : "allow" },
{ "tool" : "write_file", "action" : "allow" },
{ "tool" : "read_file", "action" : "allow" },
{ "tool" : "Grep", "action" : "allow" }
]
})
}
```
## How it Works
- **Install**: Installs Sourcegraph Amp CLI using npm (installs Node.js via NVM if required)
- **Start**: Launches Amp CLI in the specified directory, wrapped with AgentAPI to enable tasks and AI interactions
- **Environment Variables**: Sets `SOURCEGRAPH_AMP_API_KEY` and `SOURCEGRAPH_AMP_START_DIRECTORY` for the CLI execution
## Troubleshooting
- If `amp` is not found, ensure `install_sourcegraph_amp = true` and your API key is valid
- Logs are written under `/home/coder/.sourcegraph-amp-module/` (`install.log`, `agentapi-start.log`) for debugging
- If `amp` is not found, ensure `install_amp = true` and your API key is valid
- Logs are written under `/home/coder/.amp-module/` (`install.log`, `agentapi-start.log`) for debugging
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
> [!IMPORTANT]
> For using **Coder Tasks** with Amp CLI, make sure to pass the `AI Prompt` parameter and set `sourcegraph_amp_api_key`.
> To use tasks with Amp CLI, create a `coder_parameter` named `"AI Prompt"` and pass its value to the amp-cli module's `ai_prompt` variable. The `folder` variable is required for the module to function correctly.
> For using **Coder Tasks** with Amp CLI, make sure to set `amp_api_key`.
> This ensures task reporting and status updates work seamlessly.
## References
@@ -43,9 +43,9 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_sourcegraph_amp: props?.skipAmpMock ? "true" : "false",
workdir: "/home/coder",
install_amp: props?.skipAmpMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
sourcegraph_amp_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
@@ -68,45 +68,94 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
setDefaultTimeout(60 * 1000);
describe("sourcegraph-amp", async () => {
describe("amp", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
test("happy-path", async () => {
const { id } = await setup();
// test("happy-path", async () => {
// const { id } = await setup();
// await execModuleScript(id);
// await expectAgentAPIStarted(id);
// });
//
// test("api-key", async () => {
// const apiKey = "test-api-key-123";
// const { id } = await setup({
// moduleVariables: {
// amp_api_key: apiKey,
// },
// });
// await execModuleScript(id);
// const resp = await readFileContainer(
// id,
// "/home/coder/.amp-module/agentapi-start.log",
// );
// expect(resp).toContain("amp_api_key provided !");
// });
//
test("install-latest-version", async () => {
const { id } = await setup({
skipAmpMock: true,
skipAgentAPIMock: true,
moduleVariables: {
amp_version: "",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("api-key", async () => {
const apiKey = "test-api-key-123";
test("install-specific-version", async () => {
const { id } = await setup({
skipAmpMock: true,
moduleVariables: {
sourcegraph_amp_api_key: apiKey,
amp_version: "0.0.1755964909-g31e083",
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain("sourcegraph_amp_api_key provided !");
expect(resp).toContain("0.0.1755964909-g31e08");
});
test("custom-folder", async () => {
const folder = "/tmp/sourcegraph-amp-test";
test("install-via-npm", async () => {
const { id } = await setup({
skipAmpMock: true,
moduleVariables: {
install_via_npm: "true",
},
});
await execModuleScript(id);
const installLog = await readFileContainer(
id,
"/home/coder/.amp-module/install.log",
);
expect(installLog).toContain("Installing Amp via npm");
const startLog = await readFileContainer(
id,
"/home/coder/.amp-module/agentapi-start.log",
);
expect(startLog).toContain("AMP version:");
});
test("custom-workdir", async () => {
const workdir = "/tmp/amp-test";
const { id } = await setup({
moduleVariables: {
folder,
workdir,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/install.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain(folder);
expect(resp).toContain(workdir);
});
test("pre-post-install-scripts", async () => {
@@ -119,39 +168,104 @@ describe("sourcegraph-amp", async () => {
await execModuleScript(id);
const preLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/pre_install.log",
"/home/coder/.amp-module/pre_install.log",
);
expect(preLog).toContain("pre-install-script");
const postLog = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/post_install.log",
"/home/coder/.amp-module/post_install.log",
);
expect(postLog).toContain("post-install-script");
});
test("system-prompt", async () => {
const prompt = "this is a system prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_SYSTEM_PROMPT: prompt,
test("instruction-prompt", async () => {
const prompt = "this is a instruction prompt for AMP";
const { id } = await setup({
moduleVariables: {
instruction_prompt: prompt,
},
});
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/SYSTEM_PROMPT.md",
);
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.config/AGENTS.md");
expect(resp).toContain(prompt);
});
test("task-prompt", async () => {
test("ai-prompt", async () => {
const prompt = "this is a task prompt for AMP";
const { id } = await setup();
await execModuleScript(id, {
SOURCEGRAPH_AMP_TASK_PROMPT: prompt,
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.sourcegraph-amp-module/agentapi-start.log",
"/home/coder/.amp-module/agentapi-start.log",
);
expect(resp).toContain(`sourcegraph amp task prompt provided : ${prompt}`);
expect(resp).toContain(`amp task prompt provided : ${prompt}`);
});
test("custom-base-config", async () => {
const customConfig = JSON.stringify({
"amp.anthropic.thinking.enabled": false,
"amp.todos.enabled": false,
"amp.tools.stopTimeout": 900,
"amp.git.commit.ampThread.enabled": true,
});
const customMcp = JSON.stringify({
"test-server": {
command: "/usr/bin/test-mcp",
args: ["--test-arg"],
type: "stdio",
},
});
const { id } = await setup({
moduleVariables: {
base_amp_config: customConfig,
mcp: customMcp,
},
});
await execModuleScript(id, {
CODER_AGENT_TOKEN: "test-token",
CODER_AGENT_URL: "http://test-url:3000",
});
const settingsContent = await readFileContainer(
id,
"/home/coder/.config/amp/settings.json",
);
const settings = JSON.parse(settingsContent);
expect(settings["amp.anthropic.thinking.enabled"]).toBe(false);
expect(settings["amp.todos.enabled"]).toBe(false);
expect(settings["amp.tools.stopTimeout"]).toBe(900);
expect(settings["amp.git.commit.ampThread.enabled"]).toBe(true);
expect(settings["amp.mcpServers"]).toBeDefined();
expect(settings["amp.mcpServers"].coder).toBeDefined();
expect(settings["amp.mcpServers"]["test-server"]).toBeDefined();
expect(settings["amp.mcpServers"]["test-server"].command).toBe(
"/usr/bin/test-mcp",
);
expect(settings["amp.mcpServers"]["test-server"].args).toEqual([
"--test-arg",
]);
});
test("default-base-config", async () => {
const { id } = await setup();
await execModuleScript(id, {
CODER_AGENT_TOKEN: "test-token",
CODER_AGENT_URL: "http://test-url:3000",
});
const settingsContent = await readFileContainer(
id,
"/home/coder/.config/amp/settings.json",
);
const settings = JSON.parse(settingsContent);
expect(settings["amp.anthropic.thinking.enabled"]).toBe(true);
expect(settings["amp.todos.enabled"]).toBe(true);
expect(settings["amp.mcpServers"]).toBeDefined();
expect(settings["amp.mcpServers"].coder).toBeDefined();
expect(settings["amp.mcpServers"].coder.command).toBe("coder");
});
});
@@ -36,28 +36,9 @@ variable "icon" {
default = "/icon/sourcegraph-amp.svg"
}
variable "folder" {
variable "workdir" {
type = string
description = "The folder to run sourcegraph_amp in."
default = "/home/coder"
}
variable "install_sourcegraph_amp" {
type = bool
description = "Whether to install sourcegraph-amp."
default = true
}
variable "sourcegraph_amp_api_key" {
type = string
description = "sourcegraph-amp API Key"
default = ""
}
resource "coder_env" "sourcegraph_amp_api_key" {
agent_id = var.agent_id
name = "SOURCEGRAPH_AMP_API_KEY"
value = var.sourcegraph_amp_api_key
description = "The folder to run AMP CLI in."
}
variable "install_agentapi" {
@@ -69,21 +50,87 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.0"
default = "v0.10.0"
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Claude Code"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Amp"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Amp CLI"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing sourcegraph_amp"
description = "Custom script to run before installing amp cli"
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing sourcegraph_amp."
description = "Custom script to run after installing amp cli."
default = null
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI"
default = true
}
variable "install_amp" {
type = bool
description = "Whether to install amp cli."
default = true
}
variable "install_via_npm" {
type = bool
description = "Install Amp via npm instead of the official installer."
default = false
}
variable "amp_api_key" {
type = string
description = "amp cli API Key"
default = ""
}
variable "amp_version" {
type = string
description = "The version of amp cli to install."
default = ""
}
variable "ai_prompt" {
type = string
description = "Task prompt for the Amp CLI"
default = ""
}
variable "instruction_prompt" {
type = string
description = "Instruction prompt for the Amp CLI. https://ampcode.com/manual#AGENTS.md"
default = ""
}
resource "coder_env" "amp_api_key" {
agent_id = var.agent_id
name = "AMP_API_KEY"
value = var.amp_api_key
}
variable "base_amp_config" {
type = string
description = <<-EOT
@@ -102,22 +149,25 @@ variable "base_amp_config" {
default = ""
}
variable "additional_mcp_servers" {
variable "mcp" {
type = string
description = "Additional MCP servers configuration in JSON format to append to amp.mcpServers."
default = null
}
data "external" "env" {
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
}
locals {
app_slug = "amp"
default_base_config = {
default_base_config = jsonencode({
"amp.anthropic.thinking.enabled" = true
"amp.todos.enabled" = true
}
})
# Use provided config or default, then extract base settings (excluding mcpServers)
user_config = var.base_amp_config != "" ? jsondecode(var.base_amp_config) : local.default_base_config
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
base_amp_settings = { for k, v in local.user_config : k => v if k != "amp.mcpServers" }
coder_mcp = {
@@ -125,14 +175,16 @@ locals {
"command" = "coder"
"args" = ["exp", "mcp", "server"]
"env" = {
"CODER_MCP_APP_STATUS_SLUG" = local.app_slug
"CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284"
"CODER_MCP_APP_STATUS_SLUG" = var.report_tasks == true ? local.app_slug : ""
"CODER_MCP_AI_AGENTAPI_URL" = var.report_tasks == true ? "http://localhost:3284" : ""
"CODER_AGENT_TOKEN" = data.external.env.result.CODER_AGENT_TOKEN
"CODER_AGENT_URL" = data.external.env.result.CODER_AGENT_URL
}
"type" = "stdio"
}
}
additional_mcp = var.additional_mcp_servers != null ? jsondecode(var.additional_mcp_servers) : {}
additional_mcp = var.mcp != null ? jsondecode(var.mcp) : {}
merged_mcp_servers = merge(
lookup(local.user_config, "amp.mcpServers", {}),
@@ -146,21 +198,24 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".sourcegraph-amp-module"
module_dir_name = ".amp-module"
workdir = trimsuffix(var.workdir, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.0.1"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
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 = "Sourcegraph Amp"
cli_app_slug = "${local.app_slug}-cli"
cli_app_display_name = "Sourcegraph Amp CLI"
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
@@ -173,8 +228,10 @@ module "agentapi" {
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
SOURCEGRAPH_AMP_API_KEY='${var.sourcegraph_amp_api_key}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
ARG_AMP_API_KEY='${var.amp_api_key}' \
ARG_AMP_START_DIRECTORY='${var.workdir}' \
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
@@ -185,9 +242,11 @@ module "agentapi" {
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL_SOURCEGRAPH_AMP='${var.install_sourcegraph_amp}' \
SOURCEGRAPH_AMP_START_DIRECTORY='${var.folder}' \
ARG_AMP_CONFIG="$(echo -n '${base64encode(jsonencode(local.final_config))}' | base64 -d)" \
ARG_INSTALL_AMP='${var.install_amp}' \
ARG_INSTALL_VIA_NPM='${var.install_via_npm}' \
ARG_AMP_CONFIG="${base64encode(jsonencode(local.final_config))}" \
ARG_AMP_VERSION='${var.amp_version}' \
ARG_AMP_INSTRUCTION_PROMPT='${base64encode(var.instruction_prompt)}' \
/tmp/install.sh
EOT
}
@@ -1,77 +1,119 @@
#!/bin/bash
set -euo pipefail
source "$HOME"/.bashrc
# ANSI colors
BOLD='\033[1m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ARG_INSTALL_AMP=${ARG_INSTALL_AMP:-true}
ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
ARG_AMP_VERSION=${ARG_AMP_VERSION:-}
ARG_AMP_INSTRUCTION_PROMPT=$(echo -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" | base64 -d)
ARG_AMP_CONFIG=$(echo -n "${ARG_AMP_CONFIG:-}" | base64 -d)
echo "--------------------------------"
echo "Install flag: $ARG_INSTALL_SOURCEGRAPH_AMP"
echo "Workspace: $SOURCEGRAPH_AMP_START_DIRECTORY"
printf "Install flag: %s\n" "$ARG_INSTALL_AMP"
printf "Install via npm: %s\n" "$ARG_INSTALL_VIA_NPM"
printf "Amp Version: %s\n" "$ARG_AMP_VERSION"
printf "AMP Config: %s\n" "$ARG_AMP_CONFIG"
printf "Instruction Prompt: %s\n" "$ARG_AMP_INSTRUCTION_PROMPT"
echo "--------------------------------"
# Helper function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
function install_node() {
if ! command_exists npm; then
printf "npm not found, checking for Node.js installation...\n"
if ! command_exists node; then
printf "Node.js not found, installing Node.js via NVM...\n"
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
install_amp_npm() {
printf "%s${YELLOW}Installing Amp via npm${NC}\n" "${BOLD}"
# Temporarily disable nounset (-u) for nvm to avoid PROVIDED_VERSION error
set +u
nvm install --lts
nvm use --lts
nvm alias default node
set -u
printf "Node.js installed: %s\n" "$(node --version)"
printf "npm installed: %s\n" "$(npm --version)"
else
printf "Node.js is installed but npm is not available. Please install npm manually.\n"
exit 1
fi
# Load nvm if available
# shellcheck source=/dev/null
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh"
fi
}
function install_sourcegraph_amp() {
if [ "${ARG_INSTALL_SOURCEGRAPH_AMP}" = "true" ]; then
install_node
# If nvm is not used, set up user npm global directory
if ! command_exists nvm; then
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
fi
fi
printf "%s Installing Sourcegraph AMP CLI...\n" "${BOLD}"
npm install -g @sourcegraph/amp@0.0.1754179307-gba1f97
printf "%s Successfully installed Sourcegraph AMP CLI. Version: %s\n" "${BOLD}" "$(amp --version)"
if ! command_exists node || ! command_exists npm; then
printf "${YELLOW}Warning: Node.js/npm not found. Skipping Amp installation.${NC}\n"
printf "To install Amp via npm, please install Node.js and npm first.\n"
return 1
fi
}
function setup_system_prompt() {
if [ -n "${SOURCEGRAPH_AMP_SYSTEM_PROMPT:-}" ]; then
echo "Setting Sourcegraph AMP system prompt..."
mkdir -p "$HOME/.sourcegraph-amp-module"
echo "$SOURCEGRAPH_AMP_SYSTEM_PROMPT" > "$HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
echo "System prompt saved to $HOME/.sourcegraph-amp-module/SYSTEM_PROMPT.md"
printf "Node.js version: %s\n" "$(node --version)"
printf "npm version: %s\n" "$(npm --version)"
NPM_GLOBAL_PREFIX="${HOME}/.npm-global"
if [ ! -d "$NPM_GLOBAL_PREFIX" ]; then
mkdir -p "$NPM_GLOBAL_PREFIX"
fi
npm config set prefix "$NPM_GLOBAL_PREFIX"
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
if [ -n "$ARG_AMP_VERSION" ]; then
npm install -g "@sourcegraph/amp@$ARG_AMP_VERSION"
else
echo "No system prompt provided for Sourcegraph AMP."
npm install -g "@sourcegraph/amp"
fi
if ! grep -q 'export PATH="$HOME/.npm-global/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> "$HOME/.bashrc"
fi
}
install_amp_official() {
printf "%s Installing Amp using official installer\n" "${BOLD}"
if [ -n "$ARG_AMP_VERSION" ]; then
export AMP_VERSION="$ARG_AMP_VERSION"
printf "Installing Amp version: %s\n" "$AMP_VERSION"
fi
if curl -fsSL https://ampcode.com/install.sh | bash; then
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$PATH"
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"
fi
else
printf "${YELLOW}Warning: Official installer failed. Installation skipped.${NC}\n"
return 1
fi
}
function install_amp() {
if [ "${ARG_INSTALL_AMP}" = "true" ]; then
if [ "${ARG_INSTALL_VIA_NPM}" = "true" ]; then
install_amp_npm || {
printf "${YELLOW}Amp installation via npm failed.${NC}\n"
return 0
}
else
install_amp_official || {
printf "${YELLOW}Amp installation via official installer failed.${NC}\n"
return 0
}
fi
if command_exists amp; then
printf "%s${GREEN}Successfully installed Sourcegraph Amp CLI. Version: %s${NC}\n" "${BOLD}" "$(amp --version)"
fi
else
printf "Skipping Sourcegraph Amp CLI installation (install_amp=false)\n"
fi
}
function setup_instruction_prompt() {
if [ -n "${ARG_AMP_INSTRUCTION_PROMPT:-}" ]; then
echo "Setting AMP instruction prompt..."
mkdir -p "$HOME/.config"
echo "$ARG_AMP_INSTRUCTION_PROMPT" > "$HOME/.config/AGENTS.md"
echo "Instruction prompt saved to $HOME/.config/AGENTS.md"
else
echo "No instruction prompt provided for Sourcegraph AMP."
fi
}
@@ -86,11 +128,17 @@ function configure_amp_settings() {
fi
echo "Writing AMP configuration to $SETTINGS_PATH"
printf '%s\n' "$ARG_AMP_CONFIG" > "$SETTINGS_PATH"
UPDATED_CONFIG=$(echo "$ARG_AMP_CONFIG" | jq --arg token "$CODER_AGENT_TOKEN" --arg url "$CODER_AGENT_URL" \
".[\"amp.mcpServers\"].coder.env += {
\"CODER_AGENT_TOKEN\": \"$CODER_AGENT_TOKEN\",
\"CODER_AGENT_URL\": \"$CODER_AGENT_URL\"
}")
printf "UPDATED_CONFIG: %s\n" "$UPDATED_CONFIG"
printf '%s\n' "$UPDATED_CONFIG" > "$SETTINGS_PATH"
echo "AMP configuration complete"
}
install_sourcegraph_amp
setup_system_prompt
install_amp
setup_instruction_prompt
configure_amp_settings
@@ -6,11 +6,11 @@ set -euo pipefail
source "$HOME/.bashrc"
# shellcheck source=/dev/null
if [ -f "$HOME/.nvm/nvm.sh" ]; then
source "$HOME"/.nvm/nvm.sh
else
export PATH="$HOME/.npm-global/bin:$PATH"
source "$HOME/.nvm/nvm.sh"
fi
export PATH="$HOME/.local/bin:$HOME/.amp/bin:$HOME/.npm-global/bin:$PATH"
function ensure_command() {
command -v "$1" &> /dev/null || {
echo "Error: '$1' not found." >&2
@@ -18,10 +18,21 @@ function ensure_command() {
}
}
ARG_AMP_START_DIRECTORY=${ARG_AMP_START_DIRECTORY:-"$HOME"}
ARG_AMP_API_KEY=${ARG_AMP_API_KEY:-}
ARG_AMP_TASK_PROMPT=$(echo -n "${ARG_AMP_TASK_PROMPT:-}" | base64 -d)
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
echo "--------------------------------"
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
echo "--------------------------------"
ensure_command amp
echo "AMP version: $(amp --version)"
dir="$SOURCEGRAPH_AMP_START_DIRECTORY"
dir="$ARG_AMP_START_DIRECTORY"
if [[ -d "$dir" ]]; then
echo "Using existing directory: $dir"
else
@@ -30,20 +41,23 @@ else
fi
cd "$dir"
if [ -n "$SOURCEGRAPH_AMP_API_KEY" ]; then
printf "sourcegraph_amp_api_key provided !\n"
export AMP_API_KEY=$SOURCEGRAPH_AMP_API_KEY
if [ -n "$ARG_AMP_API_KEY" ]; then
printf "amp_api_key provided !\n"
export AMP_API_KEY=$ARG_AMP_API_KEY
else
printf "sourcegraph_amp_api_key not provided\n"
printf "amp_api_key not provided\n"
fi
if [ -n "${SOURCEGRAPH_AMP_TASK_PROMPT:-}" ]; then
printf "sourcegraph amp task prompt provided : $SOURCEGRAPH_AMP_TASK_PROMPT"
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $SOURCEGRAPH_AMP_TASK_PROMPT"
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AMP_TASK_PROMPT"
else
PROMPT="$ARG_AMP_TASK_PROMPT"
fi
# Pipe the prompt into amp, which will be run inside agentapi
agentapi server --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
else
printf "No task prompt given.\n"
agentapi server --term-width=67 --term-height=1190 -- amp
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
fi
@@ -22,31 +22,16 @@ provider "docker" {}
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.0"
version = "3.0.0"
agent_id = coder_agent.main.id
folder = "/home/coder/projects"
install_claude_code = true
claude_code_version = "latest"
workdir = "/home/coder/projects"
order = 999
experiment_post_install_script = data.coder_parameter.setup_script.value
# This enables Coder Tasks
experiment_report_tasks = true
}
# You can also use a model provider, like AWS Bedrock or Vertex by replacing
# this with the special env vars from the Claude Code docs.
# see: https://docs.anthropic.com/en/docs/claude-code/third-party-integrations
variable "anthropic_api_key" {
type = string
description = "Generate one at: https://console.anthropic.com/settings/keys"
sensitive = true
}
resource "coder_env" "anthropic_api_key" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
claude_api_key = ""
ai_prompt = data.coder_parameter.ai_prompt.value
system_prompt = data.coder_parameter.system_prompt.value
model = "sonnet"
permission_mode = "plan"
post_install_script = data.coder_parameter.setup_script.value
}
# We are using presets to set the prompts, image, and set up instructions
@@ -172,23 +157,6 @@ data "coder_parameter" "preview_port" {
mutable = false
}
# Other variables for Claude Code
resource "coder_env" "claude_task_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
}
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
@@ -300,13 +268,6 @@ module "code-server" {
order = 1
}
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
}
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
@@ -321,23 +282,13 @@ module "cursor" {
agent_id = coder_agent.main.id
}
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder/projects"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
folder = "/home/coder/projects"
}
resource "docker_volume" "home_volume" {
@@ -422,4 +373,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

+1 -1
View File
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
```tf
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "2.0.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
+4 -6
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.7"
version = ">= 2.12"
}
}
}
@@ -117,7 +117,7 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
default = "v0.10.0"
}
variable "agentapi_port" {
@@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" {
group = var.cli_app_group
}
resource "coder_ai_task" "agentapi" {
sidebar_app {
id = coder_app.agentapi_web.id
}
output "task_app_id" {
value = coder_app.agentapi_web.id
}
+54 -228
View File
@@ -8,76 +8,58 @@ tags: [agent, ai, aider]
# Aider
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support.
```tf
module "aider" {
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
}
```
## Features
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
- **Project Integration**: Works with any project directory, including Git repositories
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
## Module Parameters
> [!NOTE]
> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
## Usage Examples
### Basic setup with API key
```tf
variable "anthropic_api_key" {
variable "api_key" {
type = string
description = "Anthropic API key"
description = "API key"
sensitive = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
}
```
This basic setup will:
- Install Aider in the workspace
- Create a persistent screen session named "aider"
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
- Enable task reporting (configures Aider to report tasks to Coder MCP)
### Using OpenAI with tmux
```tf
variable "openai_api_key" {
type = string
description = "OpenAI API key"
sensitive = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
version = "2.0.0"
agent_id = coder_agent.example.id
use_tmux = true
ai_provider = "openai"
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
ai_api_key = var.openai_api_key
api_key = var.api_key
ai_provider = "google"
model = "gemini"
}
```
## Prerequisites
- pipx is automatically installed if not already available
## Usage Example
```tf
data "coder_parameter" "ai_prompt" {
name = "AI Prompt"
description = "Write an initial prompt for Aider to work on."
type = "string"
default = ""
mutable = true
}
variable "gemini_api_key" {
type = string
description = "Gemini API key"
sensitive = true
}
module "aider" {
source = "registry.coder.com/coder/aider/coder"
version = "2.0.0"
agent_id = coder_agent.example.id
api_key = var.gemini_api_key
install_aider = true
workdir = "/home/coder"
ai_provider = "google"
model = "gemini"
install_agentapi = true
ai_prompt = data.coder_parameter.ai_prompt.value
system_prompt = "..."
}
```
@@ -93,174 +75,16 @@ variable "custom_api_key" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
version = "2.0.0"
agent_id = coder_agent.example.id
workdir = "/home/coder"
ai_provider = "custom"
custom_env_var_name = "MY_CUSTOM_API_KEY"
ai_model = "custom-model"
ai_api_key = var.custom_api_key
model = "custom-model"
api_key = var.custom_api_key
}
```
### Adding Custom Extensions (Experimental)
You can extend Aider's capabilities by adding custom extensions:
```tf
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
experiment_pre_install_script = <<-EOT
pip install some-custom-dependency
EOT
experiment_additional_extensions = <<-EOT
custom-extension:
args: []
cmd: custom-extension-command
description: A custom extension for Aider
enabled: true
envs: {}
name: custom-extension
timeout: 300
type: stdio
EOT
}
```
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
## Task Reporting (Experimental)
> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
Your workspace must have either `screen` or `tmux` installed to use this.
Task reporting is **enabled by default** in this module, allowing you to:
- Send an initial prompt to Aider during workspace creation
- Monitor task progress in the Coder UI
- Use the `coder_parameter` resource to collect prompts from users
### Setting up Task Reporting
To use task reporting effectively:
1. Add the Coder Login module to your template
2. Configure the necessary variables to pass the task prompt
3. Optionally add a coder_parameter to collect prompts from users
Here's a complete example:
```tf
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
variable "anthropic_api_key" {
type = string
description = "Anthropic API key"
sensitive = true
}
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Write a prompt for Aider"
mutable = true
ephemeral = true
}
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
task_prompt = data.coder_parameter.ai_prompt.value
# Optionally customize the system prompt
system_prompt = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
```
When a task prompt is provided via the `task_prompt` variable, the module automatically:
1. Combines the system prompt with the task prompt into a single message in the format:
```
SYSTEM PROMPT:
[system_prompt content]
This is your current task: [task_prompt]
```
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
3. Logs task output to `$HOME/.aider.log` for reference
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
## Using Aider in Your Workspace
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
### Session Options
You can run Aider in three different ways:
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
- Simple setup without persistent context
- Suitable for quick coding sessions
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
- Session name: "aider" (or configured via `session_name`)
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
- Set `use_tmux = true` to enable
- Session name: "aider" (or configured via `session_name`)
- Configures tmux with mouse support for shared sessions
Persistent sessions (screen/tmux) allow you to:
- Disconnect and reconnect without losing context
- Run Aider in the background while doing other work
- Switch between terminal and browser interfaces
### Available AI Providers and Models
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
@@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer
## Troubleshooting
If you encounter issues:
- If `aider` is not found, ensure `install_aider = true` and your API key is valid
- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging
- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
## References
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
- [Aider Documentation](https://aider.chat/docs)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
+114 -83
View File
@@ -1,107 +1,138 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
test,
afterEach,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
loadTestFile,
writeExecutable,
setup as setupUtil,
execModuleScript,
expectAgentAPIStarted,
} from "../../../coder/modules/agentapi/test-util";
describe("aider", async () => {
await runTerraformInit(import.meta.dir);
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
afterEach(async () => {
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
interface SetupProps {
skipAgentAPIMock?: boolean;
skipAiderMock?: 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_aider: props?.skipAiderMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
aider_model: "test-model",
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
it("configures task prompt correctly", async () => {
const testPrompt = "Add a hello world function";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
task_prompt: testPrompt,
// Place the Aider mock CLI binary inside the container
if (!props?.skipAiderMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/aider",
content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"),
});
}
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
`This is your current task: ${testPrompt}`,
);
expect(instance.script).toContain("aider --architect --yes-always");
return { id };
};
setDefaultTimeout(60 * 1000);
describe("Aider", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
it("handles custom system prompt", async () => {
const customPrompt = "Report all tasks with state: working";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
system_prompt: customPrompt,
test("happy-path", async () => {
const { id } = await setup({
moduleVariables: {
model: "gemini",
},
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(customPrompt);
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
it("handles pre and post install scripts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
experiment_pre_install_script: "echo 'Pre-install script executed'",
experiment_post_install_script: "echo 'Post-install script executed'",
test("api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
api_key: apiKey,
model: "gemini",
},
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain("Running pre-install script");
expect(instance.script).toContain("Running post-install script");
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.aider-module/agentapi-start.log",
);
expect(resp).toContain("API key provided!");
});
it("validates that use_screen and use_tmux cannot both be true", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
use_screen: true,
use_tmux: true,
test("custom-folder", async () => {
const workdir = "/tmp/aider-test";
const { id } = await setup({
moduleVariables: {
workdir,
model: "gemini",
},
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.aider-module/install.log",
);
expect(instance.script).toContain("exit 1");
expect(resp).toContain(workdir);
});
it("configures Aider with known provider and model", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "anthropic",
ai_model: "sonnet",
ai_api_key: "test-anthropic-key",
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'",
model: "gemini",
},
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
await execModuleScript(id);
const preLog = await readFileContainer(
id,
"/home/coder/.aider-module/pre_install.log",
);
expect(instance.script).toContain("--model sonnet");
expect(instance.script).toContain(
"Starting Aider using anthropic provider and model: sonnet",
);
});
it("handles custom provider with custom env var and API key", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
ai_provider: "custom",
custom_env_var_name: "MY_CUSTOM_API_KEY",
ai_model: "custom-model",
ai_api_key: "test-custom-key",
});
const instance = findResourceInstance(state, "coder_script");
expect(instance.script).toContain(
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
);
expect(instance.script).toContain("--model custom-model");
expect(instance.script).toContain(
"Starting Aider using custom provider and model: custom-model",
expect(preLog).toContain("pre-install-script");
const postLog = await readFileContainer(
id,
"/home/coder/.aider-module/post_install.log",
);
expect(postLog).toContain("post-install-script");
});
});
+165 -394
View File
@@ -36,87 +36,84 @@ variable "icon" {
default = "/icon/aider.svg"
}
variable "folder" {
variable "workdir" {
type = string
description = "The folder to run Aider in."
default = "/home/coder"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = false
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Aider"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Aider"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Aider CLI"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Aider."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Aider."
default = null
}
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.10.0"
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for Aider."
default = ""
}
# ---------------------------------------------
variable "install_aider" {
type = bool
description = "Whether to install Aider."
default = true
}
variable "aider_version" {
type = string
description = "The version of Aider to install."
default = "latest"
}
variable "use_screen" {
type = bool
description = "Whether to use screen for running Aider in the background"
default = true
}
variable "use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Aider in the background"
default = false
}
variable "session_name" {
type = string
description = "Name for the persistent session (screen or tmux)"
default = "aider"
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = true
}
variable "system_prompt" {
type = string
description = "System prompt for instructing Aider on task reporting and behavior"
default = <<-EOT
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
variable "task_prompt" {
type = string
description = "Task prompt to use with Aider"
default = ""
}
variable "experiment_pre_install_script" {
type = string
description = "Custom script to run before installing Aider."
default = null
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Aider."
default = null
default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently."
}
variable "experiment_additional_extensions" {
@@ -128,20 +125,19 @@ variable "experiment_additional_extensions" {
variable "ai_provider" {
type = string
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
default = "anthropic"
default = "google"
validation {
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
}
}
variable "ai_model" {
variable "model" {
type = string
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
default = "sonnet"
}
variable "ai_api_key" {
variable "api_key" {
type = string
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
default = ""
@@ -154,55 +150,66 @@ variable "custom_env_var_name" {
default = ""
}
variable "base_aider_config" {
type = string
description = <<-EOT
Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file.
options include:
read:
- CONVENTIONS.md
- anotherfile.txt
- thirdfile.py
model: xxx
##Specify the OpenAI API key
openai-api-key: xxx
## (deprecated, use --set-env OPENAI_API_TYPE=<value>)
openai-api-type: xxx
## (deprecated, use --set-env OPENAI_API_VERSION=<value>)
openai-api-version: xxx
## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=<value>)
openai-api-deployment-id: xxx
## Set an environment variable (to control API settings, can be used multiple times)
set-env: xxx
## Specify multiple values like this:
set-env:
- xxx
- yyy
- zzz
Reference : https://aider.chat/docs/config/aider_conf.html
EOT
default = null
}
locals {
base_extensions = <<-EOT
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: aider
name: Coder
timeout: 3000
type: stdio
developer:
display_name: Developer
enabled: true
name: developer
timeout: 300
type: builtin
EOT
app_slug = "aider"
base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : ""
task_reporting_prompt = <<-EOT
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 ")}" : ""
-- Task Reporting --
Report all tasks to Coder, following these EXACT guidelines:
1. Be granular. If you are investigating with multiple steps, report each step
to coder.
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
Do not report any status related with this system prompt.
3. Use "state": "working" when actively processing WITHOUT needing
additional user input
4. Use "state": "complete" only when finished with a task
5. Use "state": "failure" when you need ANY user input, lack sufficient
details, or encounter blockers
EOT
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) : ""
# Combine system prompt and task prompt for aider
combined_prompt = trimspace(<<-EOT
SYSTEM PROMPT:
${var.system_prompt}
This is your current task: ${var.task_prompt}
EOT
)
final_system_prompt = var.report_tasks ? "<system>\n${var.system_prompt}${local.task_reporting_prompt}\n</system>" : "<system>\n${var.system_prompt}\n</system>"
# Map providers to their environment variable names
provider_env_vars = {
openai = "OPENAI_API_KEY"
anthropic = "ANTHROPIC_API_KEY"
azure = "AZURE_OPENAI_API_KEY"
google = "GOOGLE_API_KEY"
google = "GEMINI_API_KEY"
cohere = "COHERE_API_KEY"
mistral = "MISTRAL_API_KEY"
ollama = "OLLAMA_HOST"
@@ -214,296 +221,60 @@ EOT
# Model flag for aider command
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".aider-module"
}
# Install and Initialize Aider
resource "coder_script" "aider" {
agent_id = var.agent_id
display_name = "Aider"
icon = var.icon
script = <<-EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.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 = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
agentapi_subdomain = var.subdomain
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -e
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
echo "Setting up Aider AI pair programming..."
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
exit 1
fi
mkdir -p "${var.folder}"
if [ "$(uname)" = "Linux" ]; then
echo "Checking dependencies for Linux..."
if [ "${var.use_tmux}" = "true" ]; then
if ! command_exists tmux; then
echo "Installing tmux for persistent sessions..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq tmux
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q tmux
else
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
fi
else
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
fi
else
echo "tmux is already installed, skipping installation."
fi
elif [ "${var.use_screen}" = "true" ]; then
if ! command_exists screen; then
echo "Installing screen for persistent sessions..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq screen
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q screen
else
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
fi
else
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
fi
else
echo "screen is already installed, skipping installation."
fi
fi
else
echo "This module currently only supports Linux workspaces."
exit 1
fi
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
if [ "${var.install_aider}" = "true" ]; then
echo "Installing Aider..."
if ! command_exists python3 || ! command_exists pip3; then
echo "Installing Python dependencies required for Aider..."
if command -v apt-get >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y -qq python3-pip python3-venv
else
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
elif command -v dnf >/dev/null 2>&1; then
if command -v sudo >/dev/null 2>&1; then
sudo dnf install -y -q python3-pip python3-virtualenv
else
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
fi
else
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
fi
else
echo "Python is already installed, skipping installation."
fi
if ! command_exists aider; then
curl -LsSf https://aider.chat/install.sh | sh
fi
if [ -f "$HOME/.bashrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
fi
fi
if [ -f "$HOME/.zshrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
fi
fi
fi
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
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Aider to report tasks via Coder MCP..."
mkdir -p "$HOME/.config/aider"
cat > "$HOME/.config/aider/config.yml" << EOL
${trimspace(local.combined_extensions)}
EOL
echo "Added Coder MCP extension to Aider config.yml"
fi
echo "Starting persistent Aider session..."
touch "$HOME/.aider.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH="$HOME/bin:$PATH"
if [ "${var.use_tmux}" = "true" ]; then
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in tmux session..."
# 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
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
else
# 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
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
fi
else
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in screen session..."
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
export ${local.env_var_name}=\"${var.ai_api_key}\"
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
/bin/bash
"
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
else
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
export ${local.env_var_name}=\"${var.ai_api_key}\"
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
/bin/bash
"
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
fi
fi
echo "Aider setup complete!"
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_WORKDIR='${var.workdir}' \
ARG_API_KEY='${base64encode(var.api_key)}' \
ARG_MODEL='${var.model}' \
ARG_PROVIDER='${var.ai_provider}' \
ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
/tmp/start.sh
EOT
run_on_start = true
}
# Aider CLI app
resource "coder_app" "aider_cli" {
agent_id = var.agent_id
slug = "aider"
display_name = "Aider"
icon = var.icon
command = <<-EOT
install_script = <<-EOT
#!/bin/bash
set -e
set -o errexit
set -o pipefail
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.use_tmux}" = "true" ]; then
if tmux has-session -t ${var.session_name} 2>/dev/null; then
echo "Attaching to existing Aider tmux session..."
tmux attach-session -t ${var.session_name}
else
echo "Starting new Aider tmux session..."
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
fi
elif [ "${var.use_screen}" = "true" ]; then
if ! screen -list | grep -q "${var.session_name}"; then
echo "Error: No existing Aider session found. Please wait for the script to start it."
exit 1
fi
screen -xRR ${var.session_name}
else
cd "${var.folder}"
echo "Starting Aider directly..."
export ${local.env_var_name}="${var.ai_api_key}"
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
fi
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_WORKDIR='${var.workdir}' \
ARG_INSTALL_AIDER='${var.install_aider}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \
/tmp/install.sh
EOT
order = var.order
group = var.group
}
@@ -0,0 +1,149 @@
run "test_aider_basic" {
command = plan
variables {
agent_id = "test-agent-123"
workdir = "/home/coder"
model = "gemini"
}
assert {
condition = var.workdir == "/home/coder"
error_message = "Workdir variable should default to /home/coder"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.install_aider == true
error_message = "install_aider should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "install_agentapi should default to true"
}
assert {
condition = var.report_tasks == false
error_message = "report_tasks should default to false"
}
}
run "test_with_api_key" {
command = plan
variables {
agent_id = "test-agent-456"
workdir = "/home/coder/workspace"
api_key = "test-api-key-123"
model = "gemini"
}
assert {
condition = var.api_key == "test-api-key-123"
error_message = "API key value should match the input"
}
}
run "test_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
workdir = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
model = "4o"
ai_prompt = "Help me write better code"
install_aider = false
install_agentapi = false
agentapi_version = "v0.10.0"
api_key = ""
base_aider_config = "read:\n - CONVENTIONS.md"
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.model == "4o"
error_message = "Model variable should be set to '4o'"
}
assert {
condition = var.ai_prompt == "Help me write better code"
error_message = "AI prompt variable should be set correctly"
}
assert {
condition = var.install_aider == false
error_message = "install_aider should be set to false"
}
assert {
condition = var.install_agentapi == false
error_message = "install_agentapi should be set to false"
}
assert {
condition = var.agentapi_version == "v0.10.0"
error_message = "AgentAPI version should be set to 'v0.10.0'"
}
}
run "test_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
workdir = "/home/coder/scripts"
model = "gemini"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_ai_provider_env_mapping" {
command = plan
variables {
agent_id = "test-agent-provider"
workdir = "/home/coder/test"
ai_provider = "google"
model = "gemini"
custom_env_var_name = ""
}
# Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars)
assert {
condition = var.ai_provider == "google"
error_message = "AI provider should be set to 'google' for this test"
}
}
@@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
# Function to check if a command exists
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Inputs
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true}
ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-}
echo "--------------------------------"
echo "Install flag: $ARG_INSTALL_AIDER"
echo "Workspace: $ARG_WORKDIR"
echo "--------------------------------"
function install_aider() {
echo "pipx installing..."
sudo apt-get install -y pipx
echo "pipx installed!"
pipx ensurepath
mkdir -p "$ARG_WORKDIR/.local/bin"
export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH"
if ! command_exists aider; then
echo "Installing Aider via pipx..."
pipx install --force aider-install
aider-install
fi
echo "Aider installed: $(aider --version || echo 'Aider installation check failed')"
}
function configure_aider_settings() {
if [ -n "${ARG_AIDER_CONFIG}" ]; then
echo "Configuring Aider environment variables and model"
mkdir -p "$HOME/.config/aider"
echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml"
echo "Aider config created at $HOME/.config/aider/.aider.conf.yml"
else
printf "No Aider environment variables or model configured\n"
fi
}
install_aider
configure_aider_settings
@@ -0,0 +1,55 @@
#!/bin/bash
set -euo pipefail
# Ensure pipx-installed apps are in PATH
export PATH="$HOME/.local/bin:$PATH"
ARG_WORKDIR=${ARG_WORKDIR:-/home/coder}
ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d)
ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "")
ARG_MODEL=${ARG_MODEL:-}
ARG_PROVIDER=${ARG_PROVIDER:-}
ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-}
echo "--------------------------------"
echo "Provider: $ARG_PROVIDER"
echo "Model: $ARG_MODEL"
echo "--------------------------------"
if [ -n "$ARG_API_KEY" ]; then
printf "API key provided!\n"
export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY
else
printf "API key not provided.\n"
fi
build_initial_prompt() {
local initial_prompt=""
if [ -n "$ARG_AI_PROMPT" ]; then
if [ -n "$ARG_SYSTEM_PROMPT" ]; then
initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT"
else
initial_prompt="$ARG_AI_PROMPT"
fi
fi
echo "$initial_prompt"
}
start_agentapi() {
echo "Starting in directory: $ARG_WORKDIR"
cd "$ARG_WORKDIR"
local initial_prompt
initial_prompt=$(build_initial_prompt)
if [ -n "$initial_prompt" ]; then
echo "Starting agentapi with initial prompt"
agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
else
agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always
fi
}
# TODO: Implement MCP server for coder when Aider support MCP servers.
start_agentapi
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "HELLO: $(bash -c env)"
echo "aider version v0.86.0"
exit 0
fi
set -e
while true; do
echo "$(date) - aider-agent-mock"
sleep 15
done
+346 -70
View File
@@ -1,23 +1,26 @@
---
display_name: Amazon Q
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant with MCP integration and task reporting.
icon: ../../../../.icons/amazon-q.svg
verified: true
tags: [agent, ai, aws, amazon-q]
tags: [agent, ai, aws, amazon-q, tasks]
---
# Amazon Q
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
# Required: see below for how to generate
experiment_auth_tarball = var.amazon_q_auth_tarball
# Required: Authentication tarball (see below for generation)
auth_tarball = <<-EOF
base64encoded-tarball
EOF
}
```
@@ -25,97 +28,370 @@ module "amazon-q" {
## Prerequisites
- You must generate an authenticated Amazon Q tarball on another machine:
```sh
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
```
Paste the result into the `experiment_auth_tarball` variable.
- To run in the background, your workspace must have `screen` or `tmux` installed.
- **zstd** - Required for compressing the authentication tarball
- **Ubuntu/Debian**: `sudo apt-get install zstd`
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
- **auth_tarball** - Required for installation and authentication
<details>
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
### Authentication Tarball
**1. Install and authenticate Amazon Q on your local machine:**
You must generate an authenticated Amazon Q tarball on another machine where you have successfully logged in:
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
- Run `q login` and complete the authentication process in your terminal.
```bash
# 1. Install Amazon Q and login on your local machine
q login
**2. Locate your Amazon Q config directory:**
# 2. Generate the authentication tarball
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0
```
- The config is typically stored at `~/.local/share/amazon-q`.
Copy the output and use it as the `auth_tarball` variable.
**3. Generate the tarball:**
## Detailed Authentication Setup
- Run the following command in your terminal:
```sh
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0
```
**Step 1: Install Amazon Q locally**
**4. Copy the output:**
- Download from [AWS Amazon Q Developer](https://aws.amazon.com/q/developer/)
- Follow the installation instructions for your platform
- The command will output a long string. Copy this entire string.
**Step 2: Authenticate**
**5. Paste into your Terraform variable:**
```bash
q login
```
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
```tf
variable "amazon_q_auth_tarball" {
type = string
default = "PASTE_LONG_STRING_HERE"
}
```
Complete the authentication process in your browser.
**Note:**
**Step 3: Generate tarball**
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
- This process is required for each user who wants to use Amazon Q in their workspace.
```bash
cd ~/.local/share/amazon-q
tar -c . | zstd | base64 -w 0 > /tmp/amazon-q-auth.txt
```
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
</details>
## Examples
### Run Amazon Q in the background with tmux
**Step 4: Use in Terraform**
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_use_tmux = true
variable "amazon_q_auth_tarball" {
type = string
sensitive = true
default = "PASTE_YOUR_TARBALL_HERE"
}
```
### Enable task reporting (experimental)
> [!IMPORTANT]
>
> - Regenerate the tarball if you logout or re-authenticate
> - Each user needs their own authentication tarball
> - Keep the tarball secure as it contains authentication credentials
### Coder Tasks Integration
A `coder_parameter` named **'AI Prompt'** is required to enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks).
```tf
data "coder_parameter" "ai_prompt" {
name = "AI Prompt"
display_name = "AI Prompt"
description = "Prompt for the AI task to execute"
type = "string"
mutable = true
default = ""
}
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_report_tasks = true
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
ai_prompt = data.coder_parameter.ai_prompt.value
trust_all_tools = true
# Task reporting configuration
report_tasks = true
# Enable CLI app alongside web app
cli_app = true
web_app_display_name = "Amazon Q"
cli_app_display_name = "Q CLI"
}
```
### Run custom scripts before/after install
> [!IMPORTANT]
>
> - The parameter name must be exactly **'AI Prompt'** (case-sensitive)
> - This parameter enables the AI task workflow integration
> - The parameter value is passed to the Amazon Q module via the `ai_prompt` variable
> - Without this parameter, `coder_ai_task` resources will not function properly
>
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.2"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_pre_install_script = "echo Pre-install!"
experiment_post_install_script = "echo Post-install!"
### Default System Prompt
The module includes a simple system prompt that instructs Amazon Q:
```
You are a helpful Coding assistant. Aim to autonomously investigate
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
```
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
### Default Coder MCP Instructions
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
```
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
```
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
## Default Agent Configuration
The module includes a default agent configuration template that provides a comprehensive setup for Amazon Q integration:
```json
{
"name": "agent",
"description": "This is an default agent config",
"prompt": "${system_prompt}",
"mcpServers": {},
"tools": [
"fs_read",
"fs_write",
"execute_bash",
"use_aws",
"@coder",
"knowledge"
],
"toolAliases": {},
"allowedTools": ["fs_read", "@coder"],
"resources": [
"file://AmazonQ.md",
"file://README.md",
"file://.amazonq/rules/**/*.md"
],
"hooks": {},
"toolsSettings": {},
"useLegacyMcpJson": true
}
```
## Notes
### Configuration Details:
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
- If neither is set, Amazon Q runs in the foreground.
- For more details, see the [main.tf](./main.tf) source.
- **Tools Available:** File operations, bash execution, AWS CLI, Coder MCP integration, and knowledge base access
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
- **Allowed Tools:** By default, only `fs_read` and `@coder` are allowed (can be customized for security)
- **Resources:** Access to documentation and rule files in the workspace
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
- **System Prompt:** Dynamically populated from the `system_prompt` variable
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
You can override this configuration by providing your own JSON via the `agent_config` variable.
### Agent Name Configuration
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
- **Configuration File:** Saves the agent config as `~/.aws/amazonq/cli-agents/{agent_name}.json`
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
If no custom `agent_config` is provided, the default agent name "agent" is used.
## Usage Examples
### Basic Usage
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
}
```
This example will:
1. Download and install Amazon Q CLI v1.14.1
2. Extract authentication tarball to ~/.local/share/amazon-q
3. Configure Coder MCP integration for task reporting
4. Create default agent configuration file
5. Start Amazon Q in /home/coder directory
6. Provide web interface through AgentAPI
> [!IMPORTANT]
> By default `fs_write` tool is not allowed, which will pause the task execution
> an will wait for the prompt to approve it usage.
> To avoid this, and allow the normal task flow, user has two options:
>
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
> OR
> - Provide you own agent configuration with the tools of your choice allowed
### With Custom AI Prompt
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
trust_all_tools = true
}
```
> [!IMPORTANT]
> **_Security Notice_**
> In order to allow the tasks flow non-interactively all the tools are trusted
> This flag bypasses standard permission checks and allows Amazon Q broader access to your system than normally permitted.
> While this enables more functionality, it also means Amazon Q can potentially execute commands with the same privileges as the user running it.
> Use this module only in trusted environments and be aware of the security implications.
### With Custom Pre/Post Install Scripts
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
pre_install_script = <<-EOT
#!/bin/bash
echo "Setting up custom environment..."
# Install additional dependencies
sudo apt-get update && sudo apt-get install -y zstd
EOT
post_install_script = <<-EOT
#!/bin/bash
echo "Configuring Amazon Q settings..."
# Custom configuration commands
q settings chat.model claude-3-sonnet
EOT
}
```
### Specific Version Installation
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
amazon_q_version = "1.14.0" # Specific version
install_amazon_q = true
}
```
### Custom Agent Configuration
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
agent_config = <<-EOT
{
"name": "custom-agent",
"description": "Custom Amazon Q agent for my workspace",
"prompt": "You are a specialized DevOps assistant...",
"tools": ["fs_read", "fs_write", "execute_bash", "use_aws"]
}
EOT
}
```
### With Custom AgentAPI Configuration
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
agentapi_chat_based_path = true
agentapi_version = "v0.10.0"
}
```
### Air-Gapped Installation
For environments without direct internet access, you can host Amazon Q installation files internally and configure the module to use your internal repository:
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "2.1.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
auth_tarball = var.amazon_q_auth_tarball
# Point to internal artifact repository
q_install_url = "https://artifacts.internal.corp/amazon-q-releases"
# Use specific version available in your repository
amazon_q_version = "1.14.1"
}
```
**Prerequisites for Air-Gapped Setup:**
1. Download Amazon Q installation files from AWS and host them internally
2. Maintain the same directory structure: `{base_url}/{version}/q-{arch}-linux.zip`
3. Ensure both architectures are available:
- `q-x86_64-linux.zip` for Intel/AMD systems
- `q-aarch64-linux.zip` for ARM systems
4. Configure network access from Coder workspaces to your internal repository
## Troubleshooting
### Common Issues
**Authentication issues:**
- Regenerate the auth tarball on your local machine
- Ensure the tarball is properly base64 encoded
- Check that the original authentication is still valid
**MCP integration not working:**
- Verify that AgentAPI is installed (`install_agentapi = true`)
- Check that the Coder agent is properly configured
- Review the system prompt configuration
@@ -0,0 +1,372 @@
run "required_variables" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
}
}
run "minimal_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdA==" # base64 "test"
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq'"
}
}
# Test Case 1: Basic Usage No Autonomous Use of Q
# Using vanilla Kubernetes Deployment Template configuration
run "test_case_1_basic_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for basic usage"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq' for basic usage"
}
# AgentAPI is installed and configured (default behavior)
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for authentication"
}
# Foundational configuration applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with foundational configuration"
}
# No additional parameters required (using defaults)
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' when no custom config provided"
}
}
# Test Case 2: Autonomous Usage Autonomous Use of Q
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
run "test_case_2_autonomous_usage" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
}
# Q is installed and authenticated
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable should be configured for autonomous usage"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq' for autonomous usage"
}
# AgentAPI is installed and configured
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created for autonomous usage"
}
# Foundational configuration for all components applied
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated for autonomous usage"
}
# AI prompt is configured
assert {
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
error_message = "AI prompt should be configured correctly for autonomous usage"
}
# Default agent name when no custom config
assert {
condition = local.agent_name == "agent"
error_message = "Default agent name should be 'agent' for autonomous usage"
}
}
# Test Case 3: Extended Configuration Parameter Validation and File Rendering
# Validates extended configuration options and parameter application
run "test_case_3_extended_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
amazon_q_version = "1.14.1"
q_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
install_amazon_q = true
install_agentapi = true
agentapi_version = "v0.6.0"
trust_all_tools = true
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
pre_install_script = "echo 'Pre-install setup'"
post_install_script = "echo 'Post-install cleanup'"
agent_config = jsonencode({
name = "production-agent"
description = "Production Amazon Q agent for enterprise environment"
prompt = "You are a helpful software assistant working in a secure enterprise environment"
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
# All installation parameters are applied correctly
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug should be configured correctly with extended parameters"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball should be configured correctly with extended parameters"
}
# Custom agent configuration is loaded and referenced correctly
assert {
condition = local.agent_name == "production-agent"
error_message = "Agent name should be extracted from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Custom agent config should be processed correctly"
}
# AI prompt and system prompt are configured
assert {
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
error_message = "AI prompt should be configured correctly in extended configuration"
}
# Pre-install and post-install scripts are provided
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly for extended configuration"
}
}
run "full_config" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_amazon_q = true
install_agentapi = true
agentapi_version = "v0.5.0"
amazon_q_version = "latest"
trust_all_tools = true
ai_prompt = "Build a web application"
auth_tarball = "dGVzdA=="
order = 1
group = "AI Tools"
icon = "/icon/custom-amazon-q.svg"
pre_install_script = "echo 'pre-install'"
post_install_script = "echo 'post-install'"
agent_config = jsonencode({
name = "test-agent"
description = "Test agent configuration"
prompt = "You are a helpful AI assistant for testing."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read"]
resources = ["file://AmazonQ.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug environment variable not configured correctly"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should be 'amazonq'"
}
assert {
condition = length(resource.coder_env.auth_tarball) == 1
error_message = "Auth tarball environment variable should be created when provided"
}
}
run "auth_tarball_environment" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
}
assert {
condition = resource.coder_env.auth_tarball[0].name == "AMAZON_Q_AUTH_TARBALL"
error_message = "Auth tarball environment variable name should be 'AMAZON_Q_AUTH_TARBALL'"
}
assert {
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
error_message = "Auth tarball environment variable value should match input"
}
}
run "empty_auth_tarball" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
auth_tarball = ""
}
assert {
condition = length(resource.coder_env.auth_tarball) == 0
error_message = "Auth tarball environment variable should not be created when empty"
}
}
run "custom_system_prompt" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Custom system prompt for testing"
}
# Test that the system prompt is used in the agent config template
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated with custom system prompt"
}
}
run "install_options" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
install_amazon_q = false
install_agentapi = false
}
assert {
condition = resource.coder_env.status_slug.name == "CODER_MCP_APP_STATUS_SLUG"
error_message = "Status slug should still be configured even when install options are disabled"
}
}
run "version_configuration" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
amazon_q_version = "2.15.0"
agentapi_version = "v0.4.0"
}
assert {
condition = resource.coder_env.status_slug.value == "amazonq"
error_message = "Status slug value should remain 'amazonq' regardless of version"
}
}
# Additional test for agent name extraction
run "agent_name_extraction" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
agent_config = jsonencode({
name = "custom-enterprise-agent"
description = "Custom enterprise agent configuration"
prompt = "You are a custom enterprise AI assistant."
mcpServers = {}
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
toolAliases = {}
allowedTools = ["fs_read", "fs_write"]
resources = ["file://README.md"]
hooks = {}
toolsSettings = {}
useLegacyMcpJson = true
})
}
assert {
condition = local.agent_name == "custom-enterprise-agent"
error_message = "Agent name should be extracted correctly from custom agent config"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be processed correctly"
}
}
# Test for JSON encoding validation
run "json_encoding_validation" {
command = plan
variables {
agent_id = "test-agent-id"
workdir = "/tmp/test-workdir"
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
}
assert {
condition = length(local.system_prompt) > 0
error_message = "System prompt should be JSON encoded correctly"
}
assert {
condition = length(local.agent_config) > 0
error_message = "Agent config should be generated correctly with multi-line system prompt"
}
}
+509 -19
View File
@@ -2,40 +2,530 @@ import { describe, it, expect } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";
import path from "path";
const moduleDir = path.resolve(__dirname);
// Always provide agent_config to bypass template parsing issues
const baseAgentConfig = JSON.stringify({
name: "test-agent",
description: "Test agent configuration",
prompt: "You are a helpful AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const requiredVars = {
agent_id: "dummy-agent-id",
agent_config: baseAgentConfig,
workdir: "/tmp/test-workdir",
};
describe("amazon-q module", async () => {
const fullConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
install_amazon_q: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
amazon_q_version: "1.14.1",
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
trust_all_tools: false,
ai_prompt: "Build a comprehensive test suite",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
order: 1,
group: "AI Tools",
icon: "/icon/custom-amazon-q.svg",
pre_install_script: "echo 'Starting pre-install'",
post_install_script: "echo 'Completed post-install'",
agent_config: baseAgentConfig,
};
describe("amazon-q module v2.0.0", async () => {
await runTerraformInit(moduleDir);
// 1. Required variables
testRequiredVariables(moduleDir, requiredVars);
// Test Case 1: Basic Usage No Autonomous Use of Q
// Matches CDES-203 Test Case #1: Basic Usage
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
const basicUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
};
// 2. coder_script resource is created
it("creates coder_script resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const scriptResource = findResourceInstance(state, "coder_script");
expect(scriptResource).toBeDefined();
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
// Optionally, check that the script contains expected lines
expect(scriptResource.script).toContain("Installing Amazon Q");
const state = await runTerraformApply(moduleDir, basicUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// AgentAPI is installed and configured (default behavior)
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// Foundational configuration for all components is applied
// No additional parameters are required for the module to work
// Using the terminal application and Q chat returns a functional interface
});
// 3. coder_app resource is created
it("creates coder_app resource", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
expect(appResource).toBeDefined();
expect(appResource.agent_id).toBe(requiredVars.agent_id);
// Test Case 2: Autonomous Usage Autonomous Use of Q
// Matches CDES-203 Test Case 2: Autonomous Usage
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
const autonomousUsageVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
ai_prompt:
"Help me set up a Python FastAPI project with proper testing structure",
};
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
// Q is installed and authenticated
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// AgentAPI is installed and configured
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
// AI prompt is passed through from external source
// The Chat interface functions as required
// The Tasks interface functions as required
// The template can be invoked from GitHub integration as expected
});
// Add more state-based tests as needed
// Test Case 3: Extended Configuration Parameter Validation and File Rendering
// Matches CDES-203 Test Case 3: Extended Configuration
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
const extendedConfigVars = {
agent_id: "dummy-agent-id",
workdir: "/tmp/test-workdir",
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
amazon_q_version: "1.14.1",
q_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
install_amazon_q: true,
install_agentapi: true,
agentapi_version: "v0.6.0",
trust_all_tools: true,
ai_prompt:
"Help me create a production-grade TypeScript monorepo with testing and deployment",
system_prompt:
"You are a helpful software assistant working in a secure enterprise environment",
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
agent_config: JSON.stringify({
name: "production-agent",
description: "Production Amazon Q agent for enterprise environment",
prompt:
"You are a helpful software assistant working in a secure enterprise environment",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: [
"file://AmazonQ.md",
"file://README.md",
"file://.amazonq/rules/**/*.md",
],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
}),
};
const state = await runTerraformApply(moduleDir, extendedConfigVars);
// All installation steps execute in the correct order
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// auth_tarball is unpacked and used as expected
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
// agent_config is rendered correctly, and the name field is used as the agent's name
// The specified ai_prompt and system_prompt are respected by the Q agent
// Tools are trusted globally if trust_all_tools = true
// Files and scripts execute in proper sequence
});
// 1. Basic functionality test (replaces testRequiredVariables)
it("works with required variables", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
});
// 2. Environment variables are created correctly
it("creates required environment variables", async () => {
const state = await runTerraformApply(moduleDir, fullConfigVars);
// Check status slug environment variable
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
// Check auth tarball environment variable
const authTarballEnv = findResourceInstance(
state,
"coder_env",
"auth_tarball",
);
expect(authTarballEnv).toBeDefined();
expect(authTarballEnv.name).toBe("AMAZON_Q_AUTH_TARBALL");
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
});
// 3. Empty auth tarball handling
it("handles empty auth tarball correctly", async () => {
const noAuthVars = {
...requiredVars,
auth_tarball: "",
};
const state = await runTerraformApply(moduleDir, noAuthVars);
// Auth tarball environment variable should not be created when empty
const authTarballEnv = state.resources?.find(
(r) => r.type === "coder_env" && r.name === "auth_tarball",
);
expect(authTarballEnv).toBeUndefined();
});
// 4. Status slug is always created
it("creates status slug environment variable", async () => {
const state = await runTerraformApply(moduleDir, requiredVars);
// Status slug should always be configured
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
expect(statusSlugEnv.value).toBe("amazonq");
});
// 5. Install options configuration
it("respects install option flags", async () => {
const noInstallVars = {
...requiredVars,
install_amazon_q: false,
install_agentapi: false,
};
const state = await runTerraformApply(moduleDir, noInstallVars);
// Status slug should still be configured even when install options are disabled
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
// 6. Configurable installation URL
it("uses configurable q_install_url parameter", async () => {
const customUrlVars = {
...requiredVars,
q_install_url: "https://internal-mirror.company.com/amazon-q",
};
const state = await runTerraformApply(moduleDir, customUrlVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 7. Version configuration
it("uses specified versions", async () => {
const versionVars = {
...requiredVars,
amazon_q_version: "1.14.1",
agentapi_version: "v0.6.0",
};
const state = await runTerraformApply(moduleDir, versionVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 8. UI configuration options
it("supports UI customization options", async () => {
const uiCustomVars = {
...requiredVars,
order: 5,
group: "Custom AI Tools",
icon: "/icon/custom-amazon-q-icon.svg",
};
const state = await runTerraformApply(moduleDir, uiCustomVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 9. Pre and post install scripts
it("supports pre and post install scripts", async () => {
const scriptVars = {
...requiredVars,
pre_install_script: "echo 'Pre-install setup'",
post_install_script: "echo 'Post-install cleanup'",
};
const state = await runTerraformApply(moduleDir, scriptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 10. Valid agent_config JSON with different agent name
it("handles valid agent_config JSON with custom agent name", async () => {
const customAgentConfig = JSON.stringify({
name: "production-agent",
description: "Production Amazon Q agent",
prompt: "You are a production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const validAgentConfigVars = {
...requiredVars,
agent_config: customAgentConfig,
};
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 11. Air-gapped installation support
it("supports air-gapped installation with custom URL", async () => {
const airGappedVars = {
...requiredVars,
q_install_url: "https://artifacts.internal.corp/amazon-q-releases",
amazon_q_version: "1.14.1",
};
const state = await runTerraformApply(moduleDir, airGappedVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 12. Trust all tools configuration
it("handles trust_all_tools configuration", async () => {
const trustVars = {
...requiredVars,
trust_all_tools: true,
};
const state = await runTerraformApply(moduleDir, trustVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 13. AI prompt configuration
it("handles AI prompt configuration", async () => {
const promptVars = {
...requiredVars,
ai_prompt: "Create a comprehensive test suite for the application",
};
const state = await runTerraformApply(moduleDir, promptVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 14. Agent config with minimal structure
it("handles minimal agent config structure", async () => {
const minimalAgentConfig = JSON.stringify({
name: "minimal-agent",
description: "Minimal agent config",
prompt: "You are a minimal AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read"],
resources: ["file://README.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const minimalVars = {
...requiredVars,
agent_config: minimalAgentConfig,
};
const state = await runTerraformApply(moduleDir, minimalVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
});
// 15. JSON encoding validation for system prompts with newlines
it("handles system prompts with newlines correctly", async () => {
const multilinePromptVars = {
...requiredVars,
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
};
const state = await runTerraformApply(moduleDir, multilinePromptVars);
// Should create the basic resources without JSON parsing errors
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
// 16. Agent name extraction from custom config
it("extracts agent name from custom configuration correctly", async () => {
const customNameConfig = JSON.stringify({
name: "enterprise-production-agent",
description: "Enterprise production agent configuration",
prompt: "You are an enterprise production AI assistant.",
mcpServers: {},
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
toolAliases: {},
allowedTools: ["fs_read", "fs_write", "execute_bash"],
resources: ["file://README.md", "file://.amazonq/rules/**/*.md"],
hooks: {},
toolsSettings: {},
useLegacyMcpJson: true,
});
const customNameVars = {
...requiredVars,
agent_config: customNameConfig,
};
const state = await runTerraformApply(moduleDir, customNameVars);
// Should create the basic resources
const statusSlugEnv = findResourceInstance(
state,
"coder_env",
"status_slug",
);
expect(statusSlugEnv).toBeDefined();
expect(statusSlugEnv.value).toBe("amazonq");
});
});
+187 -236
View File
@@ -1,10 +1,12 @@
# Improved amazon-q module main.tf
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.7"
}
}
}
@@ -15,7 +17,6 @@ variable "agent_id" {
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "order" {
@@ -36,10 +37,63 @@ variable "icon" {
default = "/icon/amazon-q.svg"
}
variable "folder" {
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Amazon Q"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "AmazonQ"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "AmazonQ CLI"
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "ai_prompt" {
type = string
description = "The initial task prompt to send to Amazon Q."
default = ""
}
variable "pre_install_script" {
type = string
description = "Optional script to run before installing Amazon Q."
default = null
}
variable "post_install_script" {
type = string
description = "Optional script to run after installing Amazon Q."
default = null
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.10.0"
}
variable "workdir" {
type = string
description = "The folder to run Amazon Q in."
default = "/home/coder"
}
variable "install_amazon_q" {
@@ -51,43 +105,19 @@ variable "install_amazon_q" {
variable "amazon_q_version" {
type = string
description = "The version of Amazon Q to install."
default = "latest"
default = "1.14.1"
}
variable "experiment_use_screen" {
type = bool
description = "Whether to use screen for running Amazon Q in the background."
default = false
}
variable "experiment_use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
default = false
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_pre_install_script" {
variable "q_install_url" {
type = string
description = "Custom script to run before installing Amazon Q."
default = null
description = "Base URL for Amazon Q installation downloads."
default = "https://desktop-release.q.us-east-1.amazonaws.com"
}
variable "experiment_post_install_script" {
type = string
description = "Custom script to run after installing Amazon Q."
default = null
}
variable "experiment_auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
default = "tarball"
variable "trust_all_tools" {
type = bool
description = "Whether to trust all tools in Amazon Q."
default = false
}
variable "system_prompt" {
@@ -98,222 +128,143 @@ variable "system_prompt" {
and solve issues the user gives you and test your work, whenever possible.
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
but opt for autonomy.
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks, you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message.
- Be granular. If you are investigating with multiple steps, report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input.
- Use "state": "complete" only when finished with a task.
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing.
- Include clear and actionable steps for the user.
- Be less than 160 characters in length.
EOT
}
variable "ai_prompt" {
variable "coder_mcp_instructions" {
type = string
description = "The initial task prompt to send to Amazon Q."
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
default = <<-EOT
YOU MUST REPORT ALL TASKS TO CODER.
When reporting tasks you MUST follow these EXACT instructions:
- IMMEDIATELY report status after receiving ANY user message
- Be granular If you are investigating with multiple steps report each step to coder.
Task state MUST be one of the following:
- Use "state": "working" when actively processing WITHOUT needing additional user input
- Use "state": "complete" only when finished with a task
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
Task summaries MUST:
- Include specifics about what you're doing
- Include clear and actionable steps for the user
- Be less than 160 characters in length
EOT
}
variable "auth_tarball" {
type = string
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory."
default = ""
sensitive = true
}
variable "agent_config" {
type = string
description = "Optional Agent configuration JSON for Amazon Q."
default = null
}
variable "agentapi_chat_based_path" {
type = bool
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
default = false
}
# Expose status slug to the agent environment
resource "coder_env" "status_slug" {
agent_id = var.agent_id
name = "CODER_MCP_APP_STATUS_SLUG"
value = local.app_slug
}
# Expose auth tarball as environment variable for install script
resource "coder_env" "auth_tarball" {
count = var.auth_tarball != "" ? 1 : 0
agent_id = var.agent_id
name = "AMAZON_Q_AUTH_TARBALL"
value = var.auth_tarball
}
locals {
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) : ""
full_prompt = <<-EOT
${var.system_prompt}
app_slug = "amazonq"
workdir = trimsuffix(var.workdir, "/")
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".amazonq-module"
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
Your first task is:
# Create default agent config structure
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
system_prompt = local.system_prompt
})
${var.ai_prompt}
EOT
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
# Extract agent name from the selected config
agent_name = try(jsondecode(local.agent_config).name, "agent")
full_prompt = var.ai_prompt != null ? "${var.ai_prompt}" : ""
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
}
resource "coder_script" "amazon_q" {
agent_id = var.agent_id
display_name = "Amazon Q"
icon = var.icon
script = <<-EOT
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
agent_id = var.agent_id
folder = local.workdir
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 = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" >/dev/null 2>&1
}
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
chmod +x /tmp/start.sh
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_WORKDIR='${var.workdir}' \
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/start.sh
EOT
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
if [ "${var.install_amazon_q}" = "true" ]; then
echo "Installing Amazon Q..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
;;
"aarch64"|"arm64")
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Amazon Q for $ARCH..."
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
unzip q.zip
./q/install.sh --no-confirm
cd "$PREV_DIR"
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Amazon Q version: $(q --version)"
fi
echo "Extracting auth tarball..."
PREV_DIR="$PWD"
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
mkdir -p ~/.local/share/amazon-q
cd ~/.local/share/amazon-q
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball"
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
q mcp add --name coder --command "coder" --args "exp,mcp,server,--allowed-tools,coder_report_task" --env "CODER_MCP_APP_STATUS_SLUG=amazon-q" --scope global --force
echo "Added Coder MCP server to Amazon Q configuration"
fi
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
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
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Amazon Q in the background with tmux..."
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
tmux send-keys -t amazon-q "${local.full_prompt}"
sleep 5
tmux send-keys -t amazon-q Enter
fi
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Amazon Q in the background..."
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.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/.amazon-q.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS amazon-q bash -c '
cd ${var.folder}
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
exec bash
'
# Extremely hacky way to send the prompt to the screen session
# This will be fixed in the future, but `amazon-q` was not sending MCP
# tasks when an initial prompt is provided.
screen -S amazon-q -X stuff "${local.full_prompt}"
sleep 5
screen -S amazon-q -X stuff "^M"
else
if ! command_exists q; then
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
exit 1
fi
fi
EOT
run_on_start = true
}
resource "coder_app" "amazon_q" {
slug = "amazon-q"
display_name = "Amazon Q"
agent_id = var.agent_id
command = <<-EOT
install_script = <<-EOT
#!/bin/bash
set -e
set -o errexit
set -o pipefail
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 amazon-q 2>/dev/null; then
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux attach-session -t amazon-q
else
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
if screen -list | grep -q "amazon-q"; then
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -xRR amazon-q
else
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
fi
else
cd ${var.folder}
q chat --trust-all-tools
fi
EOT
icon = var.icon
order = var.order
group = var.group
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_INSTALL='${var.install_amazon_q}' \
ARG_VERSION='${var.amazon_q_version}' \
ARG_Q_INSTALL_URL='${var.q_install_url}' \
ARG_AUTH_TARBALL='${var.auth_tarball}' \
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
ARG_AGENT_NAME='${local.agent_name}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
/tmp/install.sh
EOT
}
@@ -0,0 +1,159 @@
#!/bin/bash
# Install script for amazon-q module
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Inputs
ARG_INSTALL=${ARG_INSTALL:-true}
ARG_VERSION=${ARG_VERSION:-latest}
ARG_Q_INSTALL_URL=${ARG_Q_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/.amazonq}
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Decode base64 inputs
ARG_AGENT_CONFIG_DECODED=""
if [ -n "$ARG_AGENT_CONFIG" ]; then
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
fi
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
fi
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "q_install_url: $ARG_Q_INSTALL_URL"
echo "agent_name: $ARG_AGENT_NAME"
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
echo "report_tasks: ${ARG_REPORT_TASKS}"
echo "--------------------------------"
# Install Amazon Q if requested
function install_amazon_q() {
if [ "$ARG_INSTALL" = "true" ]; then
echo "Installing Amazon Q..."
PREV_DIR="$PWD"
TMP_DIR="$(mktemp -d)"
cd "$TMP_DIR"
ARCH="$(uname -m)"
case "$ARCH" in
"x86_64")
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-x86_64-linux.zip"
;;
"aarch64" | "arm64")
Q_URL="${ARG_Q_INSTALL_URL}/${ARG_VERSION}/q-aarch64-linux.zip"
;;
*)
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
exit 1
;;
esac
echo "Downloading Amazon Q for $ARCH from $Q_URL..."
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
unzip q.zip
./q/install.sh --no-confirm
cd "$PREV_DIR"
rm -rf "$TMP_DIR"
# Ensure binaries are discoverable; create stable symlink to q
CANDIDATES=(
"$(command -v q || true)"
"$HOME/.local/bin/q"
)
FOUND_BIN=""
for c in "${CANDIDATES[@]}"; do
if [ -n "$c" ] && [ -x "$c" ]; then
FOUND_BIN="$c"
break
fi
done
export PATH="$PATH:$HOME/.local/bin"
echo "Installed Amazon Q at: $(command -v q || true) (resolved: $FOUND_BIN)"
fi
}
# Extract authentication tarball
function extract_auth_tarball() {
if [ -n "$ARG_AUTH_TARBALL" ]; then
echo "Extracting auth tarball..."
if ! command_exists zstd; then
echo "Error: zstd is required to extract the authentication tarball but is not installed."
echo "Please install zstd using the pre_install_script parameter."
exit 1
fi
PREV_DIR="$PWD"
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
rm -rf ~/.local/share/amazon-q
mkdir -p ~/.local/share/amazon-q
cd ~/.local/share/amazon-q
tar -I zstd -xf /tmp/auth.tar.zst
rm /tmp/auth.tar.zst
cd "$PREV_DIR"
echo "Extracted auth tarball to ~/.local/share/amazon-q"
else
echo "Warning: No auth tarball provided. Amazon Q may require manual authentication."
fi
}
# Configure MCP integration and create agent
function configure_agent() {
# Create Amazon Q agent configuration directory
AGENT_CONFIG_DIR="$HOME/.aws/amazonq/cli-agents"
mkdir -p "$AGENT_CONFIG_DIR"
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
echo "Applying custom MCP configuration..."
# Use agent name as filename for the configuration
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
fi
if [ "$ARG_REPORT_TASKS" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
q mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
else
q mcp add --name coder \
--command "coder" \
--agent "$ARG_AGENT_NAME" \
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
--force || echo "Warning: Failed to add Coder MCP server"
fi
echo "Added Coder MCP server into $ARG_AGENT_NAME in Amazon Q configuration"
q settings chat.defaultAgent "$ARG_AGENT_NAME"
}
# Main execution
install_amazon_q
extract_auth_tarball
configure_agent
echo "Amazon Q installation and configuration complete!"
@@ -0,0 +1,67 @@
#!/bin/bash
# Start script for amazon-q module
set -o errexit
set -o pipefail
command_exists() {
command -v "$1" > /dev/null 2>&1
}
# Decode inputs
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.aws/amazonq}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
echo "--------------------------------"
echo "ai_prompt: $ARG_AI_PROMPT"
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "workdir: $ARG_WORKDIR"
echo "report_tasks: ${ARG_REPORT_TASKS}"
echo "--------------------------------"
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
# Find Amazon Q CLI
if command_exists q; then
Q_CMD=q
elif [ -x "$HOME/.local/bin/q" ]; then
Q_CMD="$HOME/.local/bin/q"
else
echo "Error: Amazon Q CLI not found. Install it or set install_amazon_q=true."
exit 1
fi
mkdir -p "$ARG_WORKDIR"
cd "$ARG_WORKDIR"
# Set up environment
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Build command arguments
ARGS=(chat)
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
ARGS+=(--trust-all-tools)
fi
# Log and run with agentapi integration
printf "Running: %q %s\n" "$Q_CMD" "$(printf '%q ' "${ARGS[@]}")"
# If we have an AI prompt, we need to handle it specially
if [ -n "$ARG_AI_PROMPT" ]; then
if [ "$ARG_REPORT_TASKS" == "true" ]; then
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
else
PROMPT="$ARG_AI_PROMPT"
fi
ARGS+=("$PROMPT")
fi
# Use agentapi to manage the interactive session with initial prompt
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$Q_CMD" "${ARGS[@]}"
@@ -0,0 +1,27 @@
{
"name": "agent",
"description": "This is an default agent config",
"prompt": ${system_prompt},
"mcpServers": {},
"tools": [
"fs_read",
"fs_write",
"execute_bash",
"use_aws",
"@coder",
"knowledge"
],
"toolAliases": {},
"allowedTools": [
"fs_read",
"@coder"
],
"resources": [
"file://AmazonQ.md",
"file://README.md",
"file://.amazonq/rules/**/*.md"
],
"hooks": {},
"toolsSettings": {},
"useLegacyMcpJson": true
}
+266 -65
View File
@@ -1,117 +1,318 @@
---
display_name: Claude Code
description: Run Claude Code in your workspace
description: Run the Claude Code agent in your workspace.
icon: ../../../../.icons/claude.svg
verified: true
tags: [agent, claude-code, ai, tasks]
tags: [agent, claude-code, ai, tasks, anthropic]
---
# Claude Code
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks.
Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for task reporting in the Coder UI.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "latest"
source = "registry.coder.com/coder/claude-code/coder"
version = "3.3.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
}
```
> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag
> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While
> this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as
> the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
> [!WARNING]
> **Security Notice**: This module uses the `--dangerously-skip-permissions` flag when running Claude Code tasks. This flag bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
> [!NOTE]
> By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
## Prerequisites
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
- An **Anthropic API key** or a _Claude Session Token_ is required for tasks.
- You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard).
- You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription)
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
### Session Resumption Behavior
By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false`
## Examples
### Run in the background and report tasks (Experimental)
### Usage with Agent Boundaries
> This functionality is in early access as of Coder v2.21 and is still evolving.
> For now, we recommend testing it in a demo or staging environment,
> rather than deploying to production
>
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
>
> Join our [Discord channel](https://discord.gg/coder) or
> [contact us](https://coder.com/contact) to get help or share feedback.
This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access.
```tf
variable "anthropic_api_key" {
type = string
description = "The Anthropic API key"
sensitive = true
module "claude-code" {
source = "dev.registry.coder.com/coder/claude-code/coder"
enable_boundary = true
boundary_version = "main"
boundary_log_dir = "/tmp/boundary_logs"
boundary_log_level = "WARN"
boundary_additional_allowed_urls = ["GET *google.com"]
boundary_proxy_port = "8087"
version = "3.3.2"
}
```
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
### Usage with Tasks and Advanced Configuration
This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings.
```tf
data "coder_parameter" "ai_prompt" {
type = "string"
name = "AI Prompt"
default = ""
description = "Write a prompt for Claude Code"
description = "Initial task prompt for Claude Code."
mutable = true
}
# Set the prompt and system prompt for Claude Code via environment variables
resource "coder_agent" "main" {
# ...
env = {
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
CODER_MCP_APP_STATUS_SLUG = "claude-code"
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
You are a helpful assistant that can help with code.
EOT
}
}
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "1.0.40"
source = "registry.coder.com/coder/claude-code/coder"
version = "3.3.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
# Enable experimental features
experiment_report_tasks = true
claude_api_key = "xxxx-xxxxx-xxxx"
# OR
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
claude_code_version = "1.0.82" # Pin to a specific version
agentapi_version = "v0.10.0"
ai_prompt = data.coder_parameter.ai_prompt.value
model = "sonnet"
permission_mode = "plan"
mcp = <<-EOF
{
"mcpServers": {
"my-custom-tool": {
"command": "my-tool-server"
"args": ["--port", "8080"]
}
}
}
EOF
}
```
## Run standalone
### Standalone Mode
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI.
Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.2.0"
version = "3.3.2"
agent_id = coder_agent.example.id
folder = "/home/coder"
workdir = "/home/coder"
install_claude_code = true
claude_code_version = "latest"
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
report_tasks = false
cli_app = true
}
```
### Usage with Claude Code Subscription
```tf
variable "claude_code_oauth_token" {
type = string
description = "Generate one using `claude setup-token` command"
sensitive = true
value = "xxxx-xxx-xxxx"
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.3.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
```
### Usage with AWS Bedrock
#### Prerequisites
AWS account with Bedrock access, Claude models enabled in Bedrock console, appropriate IAM permissions.
Configure Claude Code to use AWS Bedrock for accessing Claude models through your AWS infrastructure.
```tf
resource "coder_env" "bedrock_use" {
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_BEDROCK"
value = "1"
}
resource "coder_env" "aws_region" {
agent_id = coder_agent.example.id
name = "AWS_REGION"
value = "us-east-1" # Choose your preferred region
}
# Option 1: Using AWS credentials
variable "aws_access_key_id" {
type = string
description = "Your AWS access key ID. Create this in the AWS IAM console under 'Security credentials'."
sensitive = true
value = "xxxx-xxx-xxxx"
}
variable "aws_secret_access_key" {
type = string
description = "Your AWS secret access key. This is shown once when you create an access key in the AWS IAM console."
sensitive = true
value = "xxxx-xxx-xxxx"
}
resource "coder_env" "aws_access_key_id" {
agent_id = coder_agent.example.id
name = "AWS_ACCESS_KEY_ID"
value = var.aws_access_key_id
}
resource "coder_env" "aws_secret_access_key" {
agent_id = coder_agent.example.id
name = "AWS_SECRET_ACCESS_KEY"
value = var.aws_secret_access_key
}
# Option 2: Using Bedrock API key (simpler)
variable "aws_bearer_token_bedrock" {
type = string
description = "Your AWS Bedrock bearer token. This provides access to Bedrock without needing separate access key and secret key."
sensitive = true
value = "xxxx-xxx-xxxx"
}
resource "coder_env" "bedrock_api_key" {
agent_id = coder_agent.example.id
name = "AWS_BEARER_TOKEN_BEDROCK"
value = var.aws_bearer_token_bedrock
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.3.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
}
```
> [!NOTE]
> For additional Bedrock configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Bedrock documentation](https://docs.claude.com/en/docs/claude-code/amazon-bedrock).
### Usage with Google Vertex AI
#### Prerequisites
GCP project with Vertex AI API enabled, Claude models enabled through Model Garden, service account with Vertex AI permissions, appropriate IAM permissions (Vertex AI User role).
Configure Claude Code to use Google Vertex AI for accessing Claude models through Google Cloud Platform.
```tf
variable "vertex_sa_json" {
type = string
description = "The complete JSON content of your Google Cloud service account key file. Create a service account in the GCP Console under 'IAM & Admin > Service Accounts', then create and download a JSON key. Copy the entire JSON content into this variable."
sensitive = true
}
resource "coder_env" "vertex_use" {
agent_id = coder_agent.example.id
name = "CLAUDE_CODE_USE_VERTEX"
value = "1"
}
resource "coder_env" "vertex_project_id" {
agent_id = coder_agent.example.id
name = "ANTHROPIC_VERTEX_PROJECT_ID"
value = "your-gcp-project-id"
}
resource "coder_env" "cloud_ml_region" {
agent_id = coder_agent.example.id
name = "CLOUD_ML_REGION"
value = "global"
}
resource "coder_env" "vertex_sa_json" {
agent_id = coder_agent.example.id
name = "VERTEX_SA_JSON"
value = var.vertex_sa_json
}
resource "coder_env" "google_application_credentials" {
agent_id = coder_agent.example.id
name = "GOOGLE_APPLICATION_CREDENTIALS"
value = "/tmp/gcp-sa.json"
}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.3.2"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
pre_install_script = <<-EOT
#!/bin/bash
# Write the service account JSON to a file
echo "$VERTEX_SA_JSON" > /tmp/gcp-sa.json
# Install prerequisite packages
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
# Add Google Cloud public key
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
# Add Google Cloud SDK repo to apt sources
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list
# Update and install the Google Cloud SDK
sudo apt-get update && sudo apt-get install -y google-cloud-cli
# Authenticate gcloud with the service account
gcloud auth activate-service-account --key-file=/tmp/gcp-sa.json
EOT
}
```
> [!NOTE]
> For additional Vertex AI configuration options (model selection, token limits, region overrides, etc.), see the [Claude Code Vertex AI documentation](https://docs.claude.com/en/docs/claude-code/google-vertex-ai).
## Troubleshooting
The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information.
If you encounter any issues, check the log files in the `~/.claude-module` directory within your workspace for detailed information.
```bash
# Installation logs
cat ~/.claude-module/install.log
# Startup logs
cat ~/.claude-module/agentapi-start.log
# Pre/post install script logs
cat ~/.claude-module/pre_install.log
cat ~/.claude-module/post_install.log
```
> [!NOTE]
> To use tasks with Claude Code, you must provide an `anthropic_api_key` or `claude_code_oauth_token`.
> The `workdir` variable is required and specifies the directory where Claude Code will run.
## References
- [Claude Code Documentation](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
- [AgentAPI Documentation](https://github.com/coder/agentapi)
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
+245 -260
View File
@@ -1,37 +1,26 @@
import {
test,
afterEach,
expect,
describe,
setDefaultTimeout,
beforeAll,
expect,
} from "bun:test";
import path from "path";
import { execContainer, readFileContainer, runTerraformInit } from "~test";
import {
execContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
writeCoder,
writeFileContainer,
} from "~test";
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) {
@@ -43,298 +32,294 @@ afterEach(async () => {
}
});
const setupContainer = async ({
image,
vars,
}: {
image?: string;
vars?: Record<string, string>;
} = {}) => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...vars,
});
const coderScript = findResourceInstance(state, "coder_script");
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
registerCleanup(() => removeContainer(id));
return { id, coderScript };
};
const loadTestFile = async (...relativePath: string[]) => {
return await Bun.file(
path.join(import.meta.dir, "testdata", ...relativePath),
).text();
};
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"],
);
};
const writeAgentAPIMockControl = async ({
containerId,
content,
}: {
containerId: string;
content: string;
}) => {
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
user: "coder",
});
};
interface SetupProps {
skipAgentAPIMock?: boolean;
skipClaudeMock?: boolean;
moduleVariables?: Record<string, string>;
agentapiMockScript?: string;
}
const projectDir = "/home/coder/project";
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const { id, coderScript } = await setupContainer({
vars: {
experiment_report_tasks: "true",
const projectDir = "/home/coder/project";
const { id } = await setupUtil({
moduleDir: import.meta.dir,
moduleVariables: {
install_claude_code: props?.skipClaudeMock ? "true" : "false",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
install_claude_code: "false",
agentapi_version: "preview",
folder: projectDir,
workdir: projectDir,
...props?.moduleVariables,
},
registerCleanup,
projectDir,
skipAgentAPIMock: props?.skipAgentAPIMock,
agentapiMockScript: props?.agentapiMockScript,
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
// the module script assumes that there is a coder executable in the PATH
await writeCoder(id, await loadTestFile("coder-mock.js"));
if (!props?.skipAgentAPIMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/agentapi",
content: await loadTestFile("agentapi-mock.js"),
});
}
if (!props?.skipClaudeMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/claude",
content: await loadTestFile("claude-mock.js"),
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
});
}
await writeExecutable({
containerId: id,
filePath: "/home/coder/script.sh",
content: coderScript.script,
});
return { id };
};
const expectAgentAPIStarted = async (id: string) => {
const resp = await execContainer(id, [
"bash",
"-c",
`curl -fs -o /dev/null "http://localhost:3284/status"`,
]);
if (resp.exitCode !== 0) {
console.log("agentapi not started");
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
};
const execModuleScript = async (id: string) => {
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`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
return resp;
};
// 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("claude-code", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
// test that the script runs successfully if claude starts without any errors
test("happy-path", async () => {
const { id } = await setup();
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("install-claude-code-version", async () => {
const version_to_install = "1.0.40";
const { id } = await setup({
skipClaudeMock: true,
moduleVariables: {
install_claude_code: "true",
claude_code_version: version_to_install,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/install.log",
]);
expect(resp.stdout).toContain(version_to_install);
});
test("check-latest-claude-code-version-works", async () => {
const { id } = await setup({
skipClaudeMock: true,
skipAgentAPIMock: true,
moduleVariables: {
install_claude_code: "true",
},
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
});
test("claude-api-key", async () => {
const apiKey = "test-api-key-123";
const { id } = await setup({
moduleVariables: {
claude_api_key: apiKey,
},
});
await execModuleScript(id);
const envCheck = await execContainer(id, [
"bash",
"-c",
'env | grep CLAUDE_API_KEY || echo "CLAUDE_API_KEY not found"',
]);
expect(envCheck.stdout).toContain("CLAUDE_API_KEY");
});
test("claude-mcp-config", async () => {
const mcpConfig = JSON.stringify({
mcpServers: {
test: {
command: "test-cmd",
type: "stdio",
},
},
});
const { id } = await setup({
skipClaudeMock: true,
moduleVariables: {
mcp: mcpConfig,
},
});
await execModuleScript(id);
const resp = await readFileContainer(id, "/home/coder/.claude.json");
expect(resp).toContain("test-cmd");
});
test("claude-task-prompt", async () => {
const prompt = "This is a task prompt for Claude.";
const { id } = await setup({
moduleVariables: {
ai_prompt: prompt,
},
});
await execModuleScript(id);
const resp = await execContainer(id, [
"bash",
"-c",
"sudo /home/coder/script.sh",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
expect(resp.stdout).toContain(prompt);
});
// test that the script removes lastSessionId from the .claude.json file
test("last-session-id-removed", async () => {
const { id } = await setup();
test("claude-permission-mode", async () => {
const mode = "plan";
const { id } = await setup({
moduleVariables: {
permission_mode: mode,
ai_prompt: "test prompt",
},
});
await execModuleScript(id);
await writeFileContainer(
id,
"/home/coder/.claude.json",
JSON.stringify({
projects: {
[projectDir]: {
lastSessionId: "123",
},
},
}),
);
const catResp = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude.json",
]);
expect(catResp.exitCode).toBe(0);
expect(catResp.stdout).toContain("lastSessionId");
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const catResp2 = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude.json",
]);
expect(catResp2.exitCode).toBe(0);
expect(catResp2.stdout).not.toContain("lastSessionId");
});
// test that the script handles a .claude.json file that doesn't contain
// a lastSessionId field
test("last-session-id-not-found", async () => {
const { id } = await setup();
await writeFileContainer(
id,
"/home/coder/.claude.json",
JSON.stringify({
projects: {
"/home/coder": {},
},
}),
);
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const catResp = await execContainer(id, [
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(catResp.exitCode).toBe(0);
expect(catResp.stdout).toContain(
"No lastSessionId found in .claude.json - nothing to do",
);
expect(startLog.stdout).toContain(`--permission-mode ${mode}`);
});
// test that if claude fails to run with the --continue flag and returns a
// no conversation found error, then the module script retries without the flag
test("no-conversation-found", async () => {
const { id } = await setup();
await writeAgentAPIMockControl({
containerId: id,
content: "no-conversation-found",
test("claude-model", async () => {
const model = "opus";
const { id } = await setup({
moduleVariables: {
model: model,
ai_prompt: "test prompt",
},
});
// check that mocking works
const respAgentAPI = await execContainer(id, [
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"agentapi --continue",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(respAgentAPI.exitCode).toBe(1);
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
expect(startLog.stdout).toContain(`--model ${model}`);
});
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);
});
// the coder binary should be executed with specific env vars
// that are set by the module script
test("coder-env-vars", async () => {
const { id } = await setup();
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
const respCoderMock = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/coder-mock-output.json",
]);
if (respCoderMock.exitCode !== 0) {
console.log(respCoderMock.stdout);
console.log(respCoderMock.stderr);
}
expect(respCoderMock.exitCode).toBe(0);
expect(JSON.parse(respCoderMock.stdout)).toEqual({
statusSlug: "ccw",
agentApiUrl: "http://localhost:3284",
test("claude-continue-resume-existing-session", async () => {
const { id } = await setup({
moduleVariables: {
continue: "true",
ai_prompt: "test prompt",
},
});
// Create a mock session file with the predefined task session ID
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
await execContainer(id, ["mkdir", "-p", sessionDir]);
await execContainer(id, [
"bash",
"-c",
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
]);
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain("--resume");
expect(startLog.stdout).toContain(taskSessionId);
expect(startLog.stdout).toContain("Resuming existing task session");
});
// verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable
// set in main.tf
test("agentapi-allowed-hosts", async () => {
const { id } = await setup();
test("pre-post-install-scripts", async () => {
const { id } = await setup({
moduleVariables: {
pre_install_script: "#!/bin/bash\necho 'claude-pre-install-script'",
post_install_script: "#!/bin/bash\necho 'claude-post-install-script'",
},
});
await execModuleScript(id);
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const agentApiStartLog = await readFileContainer(
const preInstallLog = await readFileContainer(
id,
"/home/coder/agentapi-mock.log",
"/home/coder/.claude-module/pre_install.log",
);
expect(preInstallLog).toContain("claude-pre-install-script");
const postInstallLog = await readFileContainer(
id,
"/home/coder/.claude-module/post_install.log",
);
expect(postInstallLog).toContain("claude-post-install-script");
});
test("workdir-variable", async () => {
const workdir = "/home/coder/claude-test-folder";
const { id } = await setup({
skipClaudeMock: false,
moduleVariables: {
workdir,
},
});
await execModuleScript(id);
const resp = await readFileContainer(
id,
"/home/coder/.claude-module/agentapi-start.log",
);
expect(resp).toContain(workdir);
});
test("coder-mcp-config-created", async () => {
const { id } = await setup({
moduleVariables: {
install_claude_code: "false",
},
});
await execModuleScript(id);
const installLog = await readFileContainer(
id,
"/home/coder/.claude-module/install.log",
);
expect(installLog).toContain(
"Configuring Claude Code to report tasks via Coder MCP",
);
});
test("dangerously-skip-permissions", async () => {
const { id } = await setup({
moduleVariables: {
dangerously_skip_permissions: "true",
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(startLog.stdout).toContain(`--dangerously-skip-permissions`);
});
test("subdomain-false", async () => {
const { id } = await setup({
skipAgentAPIMock: true,
moduleVariables: {
subdomain: "false",
post_install_script: dedent`
#!/bin/bash
env | grep AGENTAPI_CHAT_BASE_PATH || echo "AGENTAPI_CHAT_BASE_PATH not found"
`,
},
});
await execModuleScript(id);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/post_install.log",
]);
expect(startLog.stdout).toContain(
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
);
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
});
});
+295 -212
View File
@@ -36,12 +36,72 @@ variable "icon" {
default = "/icon/claude.svg"
}
variable "folder" {
variable "workdir" {
type = string
description = "The folder to run Claude Code in."
default = "/home/coder"
}
variable "report_tasks" {
type = bool
description = "Whether to enable task reporting to Coder UI via AgentAPI"
default = true
}
variable "cli_app" {
type = bool
description = "Whether to create a CLI app for Claude Code"
default = false
}
variable "web_app_display_name" {
type = string
description = "Display name for the web app"
default = "Claude Code"
}
variable "cli_app_display_name" {
type = string
description = "Display name for the CLI app"
default = "Claude Code CLI"
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Claude Code."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Claude Code."
default = null
}
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.10.0"
}
variable "ai_prompt" {
type = string
description = "Initial task prompt for Claude Code."
default = ""
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = false
}
variable "install_claude_code" {
type = bool
description = "Whether to install Claude Code."
@@ -54,245 +114,268 @@ variable "claude_code_version" {
default = "latest"
}
variable "experiment_cli_app" {
type = bool
description = "Whether to create the CLI workspace app."
default = false
}
variable "experiment_cli_app_order" {
type = number
description = "The order of the CLI workspace app."
default = null
}
variable "experiment_cli_app_group" {
variable "claude_api_key" {
type = string
description = "The group of the CLI workspace app."
default = null
description = "The API key to use for the Claude Code server."
default = ""
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
default = false
}
variable "experiment_pre_install_script" {
variable "model" {
type = string
description = "Custom script to run before installing Claude Code."
default = null
description = "Sets the model for the current session with an alias for the latest model (sonnet or opus) or a models full name."
default = ""
}
variable "experiment_post_install_script" {
variable "resume_session_id" {
type = string
description = "Custom script to run after installing Claude Code."
default = null
description = "Resume a specific session by ID."
default = ""
}
variable "install_agentapi" {
variable "continue" {
type = bool
description = "Whether to install AgentAPI."
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
variable "dangerously_skip_permissions" {
type = bool
description = "Skip the permission prompts. Use with caution. This will be set to true if using Coder Tasks"
default = false
}
variable "subdomain" {
variable "permission_mode" {
type = string
description = "Permission mode for the cli, check https://docs.anthropic.com/en/docs/claude-code/iam#permission-modes"
default = ""
validation {
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
error_message = "interaction_mode must be one of: default, acceptEdits, plan, bypassPermissions."
}
}
variable "mcp" {
type = string
description = "MCP JSON to be added to the claude code local scope"
default = ""
}
variable "allowed_tools" {
type = string
description = "A list of tools that should be allowed without prompting the user for permission, in addition to settings.json files."
default = ""
}
variable "disallowed_tools" {
type = string
description = "A list of tools that should be disallowed without prompting the user for permission, in addition to settings.json files."
default = ""
}
variable "claude_code_oauth_token" {
type = string
description = "Set up a long-lived authentication token (requires Claude subscription). Generated using `claude setup-token` command"
sensitive = true
default = ""
}
variable "system_prompt" {
type = string
description = "The system prompt to use for the Claude Code server."
default = ""
}
variable "claude_md_path" {
type = string
description = "The path to CLAUDE.md."
default = "$HOME/.claude/CLAUDE.md"
}
variable "enable_boundary" {
type = bool
description = "Whether to use a subdomain for the Claude Code app."
default = true
description = "Whether to enable coder boundary for network filtering"
default = false
}
variable "boundary_version" {
type = string
description = "Boundary version, valid git reference should be provided (tag, commit, branch)"
default = "main"
}
variable "boundary_log_dir" {
type = string
description = "Directory for boundary logs"
default = "/tmp/boundary_logs"
}
variable "boundary_log_level" {
type = string
description = "Log level for boundary process"
default = "WARN"
}
variable "boundary_additional_allowed_urls" {
type = list(string)
description = "Additional URLs to allow through boundary (in addition to default allowed URLs)"
default = []
}
variable "boundary_proxy_port" {
type = string
description = "Port for HTTP Proxy used by Boundary"
default = "8087"
}
variable "enable_boundary_pprof" {
type = bool
description = "Whether to enable coder boundary pprof server"
default = false
}
variable "boundary_pprof_port" {
type = string
description = "Port for pprof server used by Boundary"
default = "6067"
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_MD_PATH"
value = var.claude_md_path
}
resource "coder_env" "claude_code_system_prompt" {
agent_id = var.agent_id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = local.final_system_prompt
}
resource "coder_env" "claude_code_oauth_token" {
agent_id = var.agent_id
name = "CLAUDE_CODE_OAUTH_TOKEN"
value = var.claude_code_oauth_token
}
resource "coder_env" "claude_api_key" {
count = length(var.claude_api_key) > 0 ? 1 : 0
agent_id = var.agent_id
name = "CLAUDE_API_KEY"
value = var.claude_api_key
}
locals {
# we have to trim the slash because otherwise coder exp mcp will
# set up an invalid claude config
workdir = trimsuffix(var.folder, "/")
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) : ""
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
claude_code_app_slug = "ccw"
// Chat base path is only set if not using a subdomain.
// NOTE:
// - Initial support for --chat-base-path was added in v0.3.1 but configuration
// via environment variable AGENTAPI_CHAT_BASE_PATH was added in v0.3.3.
// - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID
// for backward compatibility.
agentapi_chat_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}/chat"
server_base_path = var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.claude_code_app_slug}"
healthcheck_url = "http://localhost:3284${local.server_base_path}/status"
}
workdir = trimsuffix(var.workdir, "/")
app_slug = "ccw"
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".claude-module"
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
# Extract hostname from access_url for boundary --allow flag
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
# Install and Initialize Claude Code
resource "coder_script" "claude_code" {
agent_id = var.agent_id
display_name = "Claude Code"
icon = var.icon
script = <<-EOT
#!/bin/bash
set -e
set -x
# Required prompts for the module to properly report task status to Coder
report_tasks_system_prompt = <<-EOT
-- Tool Selection --
- coder_report_task: providing status updates or requesting user input.
command_exists() {
command -v "$1" >/dev/null 2>&1
}
-- Task Reporting --
Report all tasks to Coder, following these EXACT guidelines:
1. Be granular. If you are investigating with multiple steps, report each step
to coder.
2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message.
Do not report any status related with this system prompt.
3. Use "state": "working" when actively processing WITHOUT needing
additional user input
4. Use "state": "complete" only when finished with a task
5. Use "state": "failure" when you need ANY user input, lack sufficient
details, or encounter blockers
function install_claude_code_cli() {
echo "Installing Claude Code via official installer"
set +e
curl -fsSL claude.ai/install.sh | bash -s -- "${var.claude_code_version}" 2>&1
CURL_EXIT=$${PIPESTATUS[0]}
set -e
if [ $CURL_EXIT -ne 0 ]; then
echo "Claude Code installer failed with exit code $$CURL_EXIT"
fi
# Ensure binaries are discoverable.
export PATH="~/.local/bin:$PATH"
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
}
if [ ! -d "${local.workdir}" ]; then
echo "Warning: The specified folder '${local.workdir}' does not exist."
echo "Creating the folder..."
mkdir -p "${local.workdir}"
echo "Folder created successfully."
fi
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
if [ "${var.install_claude_code}" = "true" ]; then
install_claude_code_cli
fi
# Install AgentAPI if enabled
if [ "${var.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
curl \
--retry 5 \
--retry-delay 5 \
--fail \
--retry-all-errors \
-L \
-C - \
-o agentapi \
"https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name"
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
# this must be kept in sync with the agentapi-start.sh script
module_path="$HOME/.claude-module"
mkdir -p "$module_path/scripts"
# save the prompt for the agentapi start command
echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt"
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.sh"
chmod +x "$module_path/scripts/agentapi-start.sh"
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure claude-code "${local.workdir}"
fi
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
if ! command_exists claude; then
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
exit 1
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
cd "${local.workdir}"
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
export AGENTAPI_ALLOWED_HOSTS="*"
# Set chat base path for non-subdomain routing (only set if not using subdomain)
export AGENTAPI_CHAT_BASE_PATH="${local.agentapi_chat_base_path}"
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
"$module_path/scripts/agentapi-wait-for-start.sh"
In your summary on coder_report_task:
- Be specific about what you're doing
- Clearly indicate what information you need from the user when in "failure" state
- Keep it under 160 characters
- Make it actionable
EOT
run_on_start = true
# Only include coder system prompts if report_tasks is enabled
custom_system_prompt = trimspace(try(var.system_prompt, ""))
final_system_prompt = format("<system>%s%s</system>",
var.report_tasks ? format("\n%s\n", local.report_tasks_system_prompt) : "",
local.custom_system_prompt != "" ? format("\n%s\n", local.custom_system_prompt) : ""
)
}
resource "coder_app" "claude_code_web" {
# use a short slug to mitigate https://github.com/coder/coder/issues/15178
slug = local.claude_code_app_slug
display_name = "Claude Code Web"
agent_id = var.agent_id
url = "http://localhost:3284/"
icon = var.icon
order = var.order
group = var.group
subdomain = var.subdomain
healthcheck {
url = local.healthcheck_url
interval = 3
threshold = 20
}
}
module "agentapi" {
resource "coder_app" "claude_code" {
count = var.experiment_cli_app ? 1 : 0
source = "registry.coder.com/coder/agentapi/coder"
version = "1.2.0"
slug = "claude-code"
display_name = "Claude Code CLI"
agent_id = var.agent_id
command = <<-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 = var.web_app_display_name
folder = local.workdir
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
agentapi_subdomain = var.subdomain
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_version = var.agentapi_version
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
chmod +x /tmp/start.sh
chmod +x /tmp/remove-last-session-id.sh
ARG_MODEL='${var.model}' \
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
ARG_CONTINUE='${var.continue}' \
ARG_DANGEROUSLY_SKIP_PERMISSIONS='${var.dangerously_skip_permissions}' \
ARG_PERMISSION_MODE='${var.permission_mode}' \
ARG_WORKDIR='${local.workdir}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \
ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \
ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
ARG_CODER_HOST='${local.coder_host}' \
/tmp/start.sh
EOT
install_script = <<-EOT
#!/bin/bash
set -e
set -o errexit
set -o pipefail
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
agentapi attach
EOT
icon = var.icon
order = var.experiment_cli_app_order
group = var.experiment_cli_app_group
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
chmod +x /tmp/install.sh
ARG_CLAUDE_CODE_VERSION='${var.claude_code_version}' \
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_INSTALL_CLAUDE_CODE='${var.install_claude_code}' \
ARG_REPORT_TASKS='${var.report_tasks}' \
ARG_WORKDIR='${local.workdir}' \
ARG_ALLOWED_TOOLS='${var.allowed_tools}' \
ARG_DISALLOWED_TOOLS='${var.disallowed_tools}' \
ARG_MCP='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
/tmp/install.sh
EOT
}
resource "coder_ai_task" "claude_code" {
sidebar_app {
id = coder_app.claude_code_web.id
}
}
@@ -0,0 +1,296 @@
run "test_claude_code_basic" {
command = plan
variables {
agent_id = "test-agent-123"
workdir = "/home/coder/projects"
}
assert {
condition = var.workdir == "/home/coder/projects"
error_message = "Workdir variable should be set correctly"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.install_claude_code == true
error_message = "Install claude_code should default to true"
}
assert {
condition = var.install_agentapi == true
error_message = "Install agentapi should default to true"
}
assert {
condition = var.report_tasks == true
error_message = "report_tasks should default to true"
}
}
run "test_claude_code_with_api_key" {
command = plan
variables {
agent_id = "test-agent-456"
workdir = "/home/coder/workspace"
claude_api_key = "test-api-key-123"
}
assert {
condition = coder_env.claude_api_key[0].value == "test-api-key-123"
error_message = "Claude API key value should match the input"
}
}
run "test_claude_code_with_custom_options" {
command = plan
variables {
agent_id = "test-agent-789"
workdir = "/home/coder/custom"
order = 5
group = "development"
icon = "/icon/custom.svg"
model = "opus"
task_prompt = "Help me write better code"
permission_mode = "plan"
continue = true
install_claude_code = false
install_agentapi = false
claude_code_version = "1.0.0"
agentapi_version = "v0.6.0"
dangerously_skip_permissions = true
}
assert {
condition = var.order == 5
error_message = "Order variable should be set to 5"
}
assert {
condition = var.group == "development"
error_message = "Group variable should be set to 'development'"
}
assert {
condition = var.icon == "/icon/custom.svg"
error_message = "Icon variable should be set to custom icon"
}
assert {
condition = var.model == "opus"
error_message = "Claude model variable should be set to 'opus'"
}
assert {
condition = var.task_prompt == "Help me write better code"
error_message = "Task prompt variable should be set correctly"
}
assert {
condition = var.permission_mode == "plan"
error_message = "Permission mode should be set to 'plan'"
}
assert {
condition = var.continue == true
error_message = "Continue should be set to true"
}
assert {
condition = var.claude_code_version == "1.0.0"
error_message = "Claude Code version should be set to '1.0.0'"
}
assert {
condition = var.agentapi_version == "v0.6.0"
error_message = "AgentAPI version should be set to 'v0.6.0'"
}
assert {
condition = var.dangerously_skip_permissions == true
error_message = "dangerously_skip_permissions should be set to true"
}
}
run "test_claude_code_with_mcp_and_tools" {
command = plan
variables {
agent_id = "test-agent-mcp"
workdir = "/home/coder/mcp-test"
mcp = jsonencode({
mcpServers = {
test = {
command = "test-server"
args = ["--config", "test.json"]
}
}
})
allowed_tools = "bash,python"
disallowed_tools = "rm"
}
assert {
condition = var.mcp != ""
error_message = "MCP configuration should be provided"
}
assert {
condition = var.allowed_tools == "bash,python"
error_message = "Allowed tools should be set"
}
assert {
condition = var.disallowed_tools == "rm"
error_message = "Disallowed tools should be set"
}
}
run "test_claude_code_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
workdir = "/home/coder/scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_claude_code_permission_mode_validation" {
command = plan
variables {
agent_id = "test-agent-validation"
workdir = "/home/coder/test"
permission_mode = "acceptEdits"
}
assert {
condition = contains(["", "default", "acceptEdits", "plan", "bypassPermissions"], var.permission_mode)
error_message = "Permission mode should be one of the valid options"
}
}
run "test_claude_code_with_boundary" {
command = plan
variables {
agent_id = "test-agent-boundary"
workdir = "/home/coder/boundary-test"
enable_boundary = true
boundary_log_dir = "/tmp/test-boundary-logs"
}
assert {
condition = var.enable_boundary == true
error_message = "Boundary should be enabled"
}
assert {
condition = var.boundary_log_dir == "/tmp/test-boundary-logs"
error_message = "Boundary log dir should be set correctly"
}
assert {
condition = local.coder_host != ""
error_message = "Coder host should be extracted from access URL"
}
}
run "test_claude_code_system_prompt" {
command = plan
variables {
agent_id = "test-agent-system-prompt"
workdir = "/home/coder/test"
system_prompt = "Custom addition"
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
assert {
condition = length(regexall("Custom addition", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have system_prompt variable value"
}
}
run "test_claude_report_tasks_default" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
# report_tasks: default is true
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
# Ensure Coder sections are injected when report_tasks=true (default)
assert {
condition = length(regexall("-- Tool Selection --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Tool Selection section"
}
assert {
condition = length(regexall("-- Task Reporting --", coder_env.claude_code_system_prompt.value)) > 0
error_message = "System prompt should have Task Reporting section"
}
}
run "test_claude_report_tasks_disabled" {
command = plan
variables {
agent_id = "test-agent-report-tasks"
workdir = "/home/coder/test"
report_tasks = false
}
assert {
condition = trimspace(coder_env.claude_code_system_prompt.value) != ""
error_message = "System prompt should not be empty"
}
# Ensure system prompt is wrapped by <system>
assert {
condition = startswith(trimspace(coder_env.claude_code_system_prompt.value), "<system>")
error_message = "System prompt should start with <system>"
}
assert {
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
error_message = "System prompt should end with </system>"
}
}
@@ -1,63 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
# this must be kept in sync with the main.tf file
module_path="$HOME/.claude-module"
scripts_dir="$module_path/scripts"
log_file_path="$module_path/agentapi.log"
# if the first argument is not empty, start claude with the prompt
if [ -n "$1" ]; then
cp "$module_path/prompt.txt" /tmp/claude-code-prompt
else
rm -f /tmp/claude-code-prompt
fi
# if the log file already exists, archive it
if [ -f "$log_file_path" ]; then
mv "$log_file_path" "$log_file_path"".$(date +%s)"
fi
# see the remove-last-session-id.sh script for details
# about why we need it
# avoid exiting if the script fails
bash "$scripts_dir/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
# we'll be manually handling errors from this point on
set +o errexit
function start_agentapi() {
local continue_flag="$1"
local prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
# visible in the terminal screen by default.
agentapi server --term-width 67 --term-height 1190 -- \
bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \
> "$log_file_path" 2>&1
}
echo "Starting AgentAPI..."
# attempt to start claude with the --continue flag
start_agentapi --continue
exit_code=$?
echo "First AgentAPI exit code: $exit_code"
if [ $exit_code -eq 0 ]; then
exit 0
fi
# if there was no conversation to continue, claude exited with an error.
# start claude without the --continue flag.
if grep -q "No conversation found to continue" "$log_file_path"; then
echo "AgentAPI with --continue flag failed, starting claude without it."
start_agentapi
exit_code=$?
fi
echo "Second AgentAPI exit code: $exit_code"
exit $exit_code
@@ -1,30 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
# 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 3284..."
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:3284/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 3284 after 15 seconds."
exit 1
fi
echo "agentapi server started on port 3284."
@@ -0,0 +1,99 @@
#!/bin/bash
set -euo pipefail
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
BOLD='\033[0;1m'
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
ARG_MCP=$(echo -n "${ARG_MCP:-}" | base64 -d)
ARG_ALLOWED_TOOLS=${ARG_ALLOWED_TOOLS:-}
ARG_DISALLOWED_TOOLS=${ARG_DISALLOWED_TOOLS:-}
echo "--------------------------------"
printf "ARG_CLAUDE_CODE_VERSION: %s\n" "$ARG_CLAUDE_CODE_VERSION"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_INSTALL_CLAUDE_CODE: %s\n" "$ARG_INSTALL_CLAUDE_CODE"
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
printf "ARG_MCP_APP_STATUS_SLUG: %s\n" "$ARG_MCP_APP_STATUS_SLUG"
printf "ARG_MCP: %s\n" "$ARG_MCP"
printf "ARG_ALLOWED_TOOLS: %s\n" "$ARG_ALLOWED_TOOLS"
printf "ARG_DISALLOWED_TOOLS: %s\n" "$ARG_DISALLOWED_TOOLS"
echo "--------------------------------"
function install_claude_code_cli() {
if [ "$ARG_INSTALL_CLAUDE_CODE" = "true" ]; then
echo "Installing Claude Code via official installer"
set +e
curl -fsSL claude.ai/install.sh | bash -s -- "$ARG_CLAUDE_CODE_VERSION" 2>&1
CURL_EXIT=${PIPESTATUS[0]}
set -e
if [ $CURL_EXIT -ne 0 ]; then
echo "Claude Code installer failed with exit code $$CURL_EXIT"
fi
# Ensure binaries are discoverable.
echo "Creating a symlink for claude"
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
else
echo "Skipping Claude Code installation as per configuration."
fi
}
function setup_claude_configurations() {
if [ ! -d "$ARG_WORKDIR" ]; then
echo "Warning: The specified folder '$ARG_WORKDIR' does not exist."
echo "Creating the folder..."
mkdir -p "$ARG_WORKDIR"
echo "Folder created successfully."
fi
module_path="$HOME/.claude-module"
mkdir -p "$module_path"
if [ "$ARG_MCP" != "" ]; then
while IFS= read -r server_name && IFS= read -r server_json; do
echo "------------------------"
echo "Executing: claude mcp add \"$server_name\" '$server_json'"
claude mcp add "$server_name" "$server_json"
echo "------------------------"
echo ""
done < <(echo "$ARG_MCP" | jq -r '.mcpServers | to_entries[] | .key, (.value | @json)')
fi
if [ -n "$ARG_ALLOWED_TOOLS" ]; then
coder --allowedTools "$ARG_ALLOWED_TOOLS"
fi
if [ -n "$ARG_DISALLOWED_TOOLS" ]; then
coder --disallowedTools "$ARG_DISALLOWED_TOOLS"
fi
}
function report_tasks() {
if [ "$ARG_REPORT_TASKS" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure claude-code "$ARG_WORKDIR"
fi
}
install_claude_code_cli
setup_claude_configurations
report_tasks
@@ -0,0 +1,178 @@
#!/bin/bash
set -euo pipefail
if [ -f "$HOME/.bashrc" ]; then
source "$HOME"/.bashrc
fi
export PATH="$HOME/.local/bin:$PATH"
command_exists() {
command -v "$1" > /dev/null 2>&1
}
ARG_MODEL=${ARG_MODEL:-}
ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-}
ARG_CONTINUE=${ARG_CONTINUE:-false}
ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-}
ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-}
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false}
ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"}
ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"}
ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"}
ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
ARG_CODER_HOST=${ARG_CODER_HOST:-}
echo "--------------------------------"
printf "ARG_MODEL: %s\n" "$ARG_MODEL"
printf "ARG_RESUME: %s\n" "$ARG_RESUME_SESSION_ID"
printf "ARG_CONTINUE: %s\n" "$ARG_CONTINUE"
printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIONS"
printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR"
printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL"
printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT"
printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
echo "--------------------------------"
# see the remove-last-session-id.sh script for details
# about why we need it
# avoid exiting if the script fails
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
function install_boundary() {
# Install boundary from public github repo
git clone https://github.com/coder/boundary
cd boundary
git checkout $ARG_BOUNDARY_VERSION
go install ./cmd/...
}
function validate_claude_installation() {
if command_exists claude; then
printf "Claude Code is installed\n"
else
printf "Error: Claude Code is not installed. Please enable install_claude_code or install it manually\n"
exit 1
fi
}
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
task_session_exists() {
if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
return 0
else
return 1
fi
}
ARGS=()
function start_agentapi() {
# For Task reporting
export CODER_MCP_ALLOWED_TOOLS="coder_report_task"
mkdir -p "$ARG_WORKDIR"
cd "$ARG_WORKDIR"
if [ -n "$ARG_MODEL" ]; then
ARGS+=(--model "$ARG_MODEL")
fi
if [ -n "$ARG_PERMISSION_MODE" ]; then
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
fi
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID"
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
elif [ "$ARG_CONTINUE" = "true" ]; then
if task_session_exists; then
echo "Task session detected (ID: $TASK_SESSION_ID)"
ARGS+=(--resume "$TASK_SESSION_ID")
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Resuming existing task session"
else
echo "No existing task session found"
ARGS+=(--session-id "$TASK_SESSION_ID")
if [ -n "$ARG_AI_PROMPT" ]; then
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
echo "Starting new task session with prompt"
else
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Starting new task session"
fi
fi
else
echo "Continue disabled, starting fresh session"
if [ -n "$ARG_AI_PROMPT" ]; then
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
echo "Starting new session with prompt"
else
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
ARGS+=(--dangerously-skip-permissions)
fi
echo "Starting claude code session"
fi
fi
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
install_boundary
mkdir -p "$ARG_BOUNDARY_LOG_DIR"
printf "Starting with coder boundary enabled\n"
# Build boundary args with conditional --unprivileged flag
BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR")
# Add default allowed URLs
BOUNDARY_ARGS+=(--allow "*anthropic.com" --allow "registry.npmjs.org" --allow "*sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST")
# Add any additional allowed URLs from the variable
if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then
IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS"
for url in "${ADDITIONAL_URLS[@]}"; do
BOUNDARY_ARGS+=(--allow "$url")
done
fi
# Set HTTP Proxy port used by Boundary
BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
# Set log level for boundary
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
# Enable boundary pprof server on specified port
BOUNDARY_ARGS+=(--pprof)
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
fi
agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \
sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
claude "${ARGS[@]}"
else
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
fi
}
validate_claude_installation
start_agentapi
@@ -1,39 +0,0 @@
#!/usr/bin/env node
const http = require("http");
const fs = require("fs");
const args = process.argv.slice(2);
const port = 3284;
const controlFile = "/tmp/agentapi-mock.control";
let control = "";
if (fs.existsSync(controlFile)) {
control = fs.readFileSync(controlFile, "utf8");
}
if (
control === "no-conversation-found" &&
args.join(" ").includes("--continue")
) {
// this must match the error message in the agentapi-start.sh script
console.error("No conversation found to continue");
process.exit(1);
}
fs.writeFileSync(
"/home/coder/agentapi-mock.log",
`AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`,
);
console.log(`starting server on port ${port}`);
http
.createServer(function (_request, response) {
response.writeHead(200);
response.end(
JSON.stringify({
status: "stable",
}),
);
})
.listen(port);
@@ -1,9 +0,0 @@
#!/usr/bin/env node
const main = async () => {
console.log("mocking claude");
// sleep for 30 minutes
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
};
main();
@@ -0,0 +1,13 @@
#!/bin/bash
if [[ "$1" == "--version" ]]; then
echo "claude version v1.0.0"
exit 0
fi
set -e
while true; do
echo "$(date) - claude-mock"
sleep 15
done
@@ -1,14 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG";
const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL";
fs.writeFileSync(
"/home/coder/coder-mock-output.json",
JSON.stringify({
statusSlug: process.env[statusSlugEnvVar] ?? "env var not set",
agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set",
}),
);
+34 -11
View File
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -43,12 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
data "coder_external_auth" "github" {
id = "github"
}
```
@@ -69,7 +69,7 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
@@ -103,7 +103,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
@@ -122,7 +122,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -134,7 +134,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
@@ -155,7 +155,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@@ -173,7 +173,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@@ -192,9 +192,32 @@ If not defined, the default, `0`, performs a full clone.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.1.0"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
}
```
## Post-clone script
Run a custom script after cloning the repository by setting the `post_clone_script` variable.
This is useful for running initialization tasks like installing dependencies or setting up the environment.
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
#!/bin/bash
echo "Repository cloned successfully!"
# Install dependencies
npm install
# Run any other initialization tasks
make setup
EOT
}
```
+18 -1
View File
@@ -30,11 +30,12 @@ describe("git-clone", async () => {
url: "fake-url",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(128);
expect(output.stdout).toEqual([
"Creating directory ~/fake-url...",
"Cloning fake-url to ~/fake-url...",
]);
expect(output.stderr.join(" ")).toContain("fatal");
expect(output.stderr.join(" ")).toContain("fake-url");
});
it("repo_dir should match repo name for https", async () => {
@@ -244,4 +245,20 @@ describe("git-clone", async () => {
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
it("runs post-clone script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
post_clone_script: "echo 'Post-clone script executed'",
});
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p ~/fake-url && echo 'existing' > ~/fake-url/file.txt",
);
expect(output.stdout).toContain("Running post-clone script...");
expect(output.stdout).toContain("Post-clone script executed");
});
});
+9
View File
@@ -62,6 +62,12 @@ variable "depth" {
default = 0
}
variable "post_clone_script" {
description = "Custom script to run after cloning the repository. Runs always after git clone, even if the repository already exists."
type = string
default = null
}
locals {
# Remove query parameters and fragments from the URL
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
@@ -81,6 +87,8 @@ locals {
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
# Construct the web URL
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
# Encode the post_clone_script for passing to the shell script
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
}
output "repo_dir" {
@@ -120,6 +128,7 @@ resource "coder_script" "git_clone" {
REPO_URL : local.clone_url,
BRANCH_NAME : local.branch_name,
DEPTH = var.depth,
POST_CLONE_SCRIPT : local.encoded_post_clone_script,
})
display_name = "Git Clone"
icon = "/icon/git.svg"
+11 -1
View File
@@ -6,6 +6,7 @@ BRANCH_NAME="${BRANCH_NAME}"
# Expand home if it's specified!
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
DEPTH="${DEPTH}"
POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
# Check if the variable is empty...
if [ -z "$REPO_URL" ]; then
@@ -52,5 +53,14 @@ if [ -z "$(ls -A "$CLONE_PATH")" ]; then
fi
else
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
exit 0
fi
# Run post-clone script if provided
if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
chmod +x /tmp/post_clone.sh
cd "$CLONE_PATH"
/tmp/post_clone.sh
rm /tmp/post_clone.sh
fi
+4 -2
View File
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "2.1.1"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
version = "2.1.1"
version = "2.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -123,4 +123,6 @@ Note: The indentation in the heredoc is preserved, so you can write the YAML nat
## Troubleshooting
By default, this module is configured to run the embedded chat interface as a path-based application. In production, we recommend that you configure a [wildcard access URL](https://coder.com/docs/admin/setup#wildcard-access-url) and set `subdomain = true`. See [here](https://coder.com/docs/tutorials/best-practices/security-best-practices#disable-path-based-apps) for more details.
The module will create log files in the workspace's `~/.goose-module` directory. If you run into any issues, look at them for more information.
+34 -14
View File
@@ -2,6 +2,7 @@ import {
test,
afterEach,
describe,
it,
setDefaultTimeout,
beforeAll,
expect,
@@ -253,22 +254,41 @@ describe("goose", async () => {
expect(prompt.stderr).toContain("No such file or directory");
});
test("subdomain-false", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
moduleVariables: {
subdomain: "false",
},
describe("subdomain", async () => {
it("sets AGENTAPI_CHAT_BASE_PATH when false", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
moduleVariables: {
subdomain: "false",
},
});
await execModuleScript(id);
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain(
"AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat",
);
});
await execModuleScript(id);
it("does not set AGENTAPI_CHAT_BASE_PATH when true", async () => {
const { id } = await setup({
agentapiMockScript: await loadTestFile(
import.meta.dir,
"agentapi-mock-print-args.js",
),
moduleVariables: {
subdomain: "true",
},
});
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toContain(
"AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/goose/chat",
);
await execModuleScript(id);
const agentapiMockOutput = await readFileContainer(id, agentapiStartLog);
expect(agentapiMockOutput).toMatch(/AGENTAPI_CHAT_BASE_PATH=$/m);
});
});
});
+5 -3
View File
@@ -63,13 +63,13 @@ variable "install_agentapi" {
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.3.3"
default = "v0.10.0"
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for AgentAPI."
default = true
default = false
}
variable "goose_provider" {
@@ -135,11 +135,12 @@ EOT
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".goose-module"
folder = trimsuffix(var.folder, "/")
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = var.agent_id
web_app_slug = local.app_slug
@@ -156,6 +157,7 @@ module "agentapi" {
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
folder = local.folder
install_script = <<-EOT
#!/bin/bash
set -o errexit
@@ -10,6 +10,9 @@ tags: [ide, jetbrains, parameter, gateway]
This module adds a JetBrains Gateway Button to open any workspace with a single click.
> [!TIP]
> We recommend using the [Coder Toolbox module](https://registry.coder.com/modules/coder/jetbrains), which offers significant stability and connectivity benefits over Gateway. Reference our [documentation](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for more information.
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
@@ -17,7 +20,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
@@ -35,7 +38,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -49,7 +52,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -64,7 +67,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -89,7 +92,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -107,7 +110,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.2"
version = "1.2.5"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -20,7 +20,7 @@ describe("jetbrains-gateway", async () => {
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent_id=foo",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=",
);
const coder_app = state.resources.find(
@@ -40,4 +40,28 @@ describe("jetbrains-gateway", async () => {
});
expect(state.outputs.identifier.value).toBe("IU");
});
it("optionally includes agent when an agent name is provided", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "main",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent=main",
);
});
it("includes the agent parameter even when the provided value is blank", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: " ",
folder: "/home/coder",
});
expect(state.outputs.url.value).toBe(
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz&agent= ",
);
});
});
@@ -30,15 +30,14 @@ variable "agent_id" {
variable "slug" {
type = string
description = "The slug for the coder_app. Allows resuing the module with the same template."
description = "The slug for the coder_app. Allows reusing the module with the same template."
default = "gateway"
}
variable "agent_name" {
type = string
description = "Agent name. (unused). Will be removed in a future version"
default = ""
description = "Agent name."
default = ""
}
variable "folder" {
@@ -348,8 +347,8 @@ resource "coder_app" "gateway" {
local.build_number,
"&ide_download_link=",
local.download_link,
"&agent_id=",
var.agent_id,
"&agent=",
var.agent_name,
])
}

Some files were not shown because too many files have changed in this diff Show More