Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .icons/open-collaboration-tools.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
111 changes: 111 additions & 0 deletions registry/edd88-pixel/modules/open-collaboration-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
display_name: Open Collaboration Tools
description: Configure live collaborative editing with Open Collaboration Tools in Coder web IDEs
icon: ../../../../.icons/open-collaboration-tools.svg
verified: false
tags: [collaboration, ide, pair-programming, vscode]
---

# Open Collaboration Tools

Configure the official Open Collaboration Tools (OCT) extension for live collaborative editing in a Coder web IDE. The module supplies settings and a versioned extension identifier that compose with the existing code-server or VS Code Web modules.

```tf
module "open_collaboration_tools" {
source = "registry.coder.com/edd88-pixel/open-collaboration-tools/coder"
version = "1.0.0"

server_url = "https://oct.example.com"
}

module "code_server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.4.1"

agent_id = coder_agent.main.id
extensions = module.open_collaboration_tools.extensions
settings = module.open_collaboration_tools.settings
}
```

![Two Coder workspaces editing the same file through Open Collaboration Tools](../../.images/open-collaboration-tools-e2e.png)

![Coder workspace list showing the OCT participant workspace running](../../.images/open-collaboration-tools-workspace-running.png)

## How Coder and OCT fit together

Coder creates and secures the workspaces and web IDE entry points. A long-lived OCT server brokers collaboration sessions between the OCT extensions running in those IDEs. This module configures only the workspace-facing extension; it does not deploy the shared server or place OAuth credentials in a workspace or Terraform state.

OCT sessions are held in memory by the server. Do not run one server per workspace, and do not rely on a room surviving a server restart.

## VS Code Web

The same outputs compose with the VS Code Web module:

```tf
module "vscode_web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.1.0"

agent_id = coder_agent.main.id
accept_license = true
extensions = module.open_collaboration_tools.extensions
settings = module.open_collaboration_tools.settings
}
```

When an IDE already contains the OCT extension, set `install_extension = false`. The module then returns an empty extension list while continuing to manage the OCT settings.

## Joining policy

The default `prompt` policy requires the host to approve every participant. An `allowlist` can admit selected usernames without prompting:

```tf
module "open_collaboration_tools" {
source = "registry.coder.com/edd88-pixel/open-collaboration-tools/coder"
version = "1.0.0"

server_url = "https://oct.example.com"
join_accept_mode = "allowlist"
join_allowlist = ["alice", "bob"]
}
```

Set `join_accept_mode = "auto"` only for a trusted environment where every authenticated OCT user may enter a hosted session without confirmation.

## Coder OAuth2 administrator setup

Coder's OAuth2 provider is experimental and should be enabled only after reviewing its current limitations. An administrator must enable the `oauth2` experiment, create an OAuth2 application, and register the exact OCT callback URL:

```text
https://oct.example.com/api/login/oauth-callback
```

Configure the external OCT service with its base URL, Coder's `/oauth2/authorize` and `/oauth2/tokens` endpoints, Coder's `/api/v2/users/me` user-info endpoint, the `username` and `email` claims, and S256 PKCE. Inject `OCT_OAUTH_CLIENTSECRET` from the deployment's secret manager; never place it in a Coder template or workspace environment.

## Create and join a session

In the host IDE, run **Open Collaboration Tools: Create Collaboration Session**. After authentication, share the invitation code through a trusted channel. In the participant IDE, run **Open Collaboration Tools: Join Collaboration Session** and enter that invitation.

The published extension also exposes `oct.createRoom` and `oct.joinRoom` to other VS Code extensions. External desktop launchers can use the OCT `vscode://` join URI, but browser-hosted IDEs should use the commands inside the extension.

## Network and restricted environments

This module runs no scripts, requires no elevated privileges, and downloads nothing itself. With extension installation enabled, the selected IDE module contacts its configured extension marketplace to obtain `typefox.open-collaboration-tools`; at runtime, the extension contacts the configured OCT server, which redirects authentication to the Coder deployment.

For restricted environments, mirror or preinstall the extension through the IDE module or workspace image, set `install_extension = false`, and allow only the Coder and OCT origins required by the deployment. The OCT server and the browser must both be able to reach the Coder OAuth2 endpoints. No public OCT service is required.

## Server health check

The OCT server does not provide a home page. Opening its root URL can therefore return `Cannot GET /` even when the service is healthy. Use the metadata endpoint for a non-authenticated connectivity check:

```shell
curl --fail --show-error https://oct.example.com/api/meta
```

During an actual session, the extension also uses `/api/login/*` for authentication and `/api/session/*` for collaboration. Those routes require the appropriate request method and session context, so `/api/meta` is the clearer standalone health probe.

> [!WARNING]
> OCT does not share terminals or forwarded ports. A session also ends when its in-memory OCT server state is lost.
63 changes: 63 additions & 0 deletions registry/edd88-pixel/modules/open-collaboration-tools/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";

describe("open-collaboration-tools", async () => {
await runTerraformInit(import.meta.dir);

testRequiredVariables(import.meta.dir, {
server_url: "https://oct.example.com",
});

it("exposes defaults for web IDE composition", async () => {
const state = await runTerraformApply(import.meta.dir, {
server_url: "https://oct.example.com",
});

expect(state.outputs.extensions.value).toEqual([
"typefox.open-collaboration-tools@0.3.9",
]);
expect(state.outputs.settings.value).toEqual({
"oct.alwaysAskToOverrideServerUrl": false,
"oct.files.exclude": ["**/.env"],
"oct.joinAcceptMode": "prompt",
"oct.joinAllowlist": [],
"oct.serverUrl": "https://oct.example.com/",
});
});

it("supports an extension already installed in the image", async () => {
const state = await runTerraformApply(import.meta.dir, {
server_url: "https://oct.example.com",
install_extension: false,
});

expect(state.outputs.extensions.value).toEqual([]);
});

it("preserves a configured collaboration policy", async () => {
const state = await runTerraformApply(import.meta.dir, {
server_url: "http://localhost:8100/api",
extension_id: "internal.open-collaboration-tools",
extension_version: "0.3.9-internal.1",
always_ask_to_override_server_url: true,
join_accept_mode: "allowlist",
join_allowlist: '["alice","bob"]',
excluded_files: '["**/.env","**/*.pem"]',
});

expect(state.outputs.extensions.value).toEqual([
"internal.open-collaboration-tools@0.3.9-internal.1",
]);
expect(state.outputs.settings.value).toEqual({
"oct.alwaysAskToOverrideServerUrl": true,
"oct.files.exclude": ["**/.env", "**/*.pem"],
"oct.joinAcceptMode": "allowlist",
"oct.joinAllowlist": ["alice", "bob"],
"oct.serverUrl": "http://localhost:8100/api/",
});
});
});
104 changes: 104 additions & 0 deletions registry/edd88-pixel/modules/open-collaboration-tools/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
terraform {
required_version = ">= 1.0"
}

variable "server_url" {
description = "URL of the Open Collaboration Tools server. HTTPS is required except for localhost development servers."
type = string

validation {
condition = (
can(regex("^https://[^/[:space:]]+(:[0-9]{1,5})?(/[^[:space:]]*)?$", var.server_url)) ||
can(regex("^http://(localhost|127\\.0\\.0\\.1)(:[0-9]{1,5})?(/[^[:space:]]*)?$", var.server_url))
)
error_message = "server_url must use HTTPS, except that HTTP is allowed for localhost or 127.0.0.1."
}
}

variable "extension_id" {
description = "Identifier of the Open Collaboration Tools extension to configure."
type = string
default = "typefox.open-collaboration-tools"

validation {
condition = can(regex("^[A-Za-z0-9][A-Za-z0-9-]*\\.[A-Za-z0-9][A-Za-z0-9-]*$", var.extension_id))
error_message = "extension_id must use the publisher.extension format."
}
}

variable "extension_version" {
description = "Version of the Open Collaboration Tools extension to install."
type = string
default = "0.3.9"

validation {
condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+([+-][0-9A-Za-z.-]+)?$", var.extension_version))
error_message = "extension_version must be a semantic version such as 0.3.9."
}
}

variable "install_extension" {
description = "Whether compatible IDE modules should install the configured extension. Disable this when the extension is already present in the workspace image."
type = bool
default = true
}

variable "always_ask_to_override_server_url" {
description = "Whether OCT should ask before switching to the server URL contained in an invitation."
type = bool
default = false
}

variable "join_accept_mode" {
description = "Policy used by a host when another user requests to join: prompt, allowlist, or auto."
type = string
default = "prompt"

validation {
condition = contains(["prompt", "allowlist", "auto"], var.join_accept_mode)
error_message = "join_accept_mode must be prompt, allowlist, or auto."
}
}

variable "join_allowlist" {
description = "Usernames allowed to join without confirmation when join_accept_mode is allowlist."
type = list(string)
default = []

validation {
condition = alltrue([for username in var.join_allowlist : trimspace(username) != ""])
error_message = "join_allowlist entries must not be empty."
}
}

variable "excluded_files" {
description = "Glob patterns for files that OCT must not share with session participants."
type = list(string)
default = ["**/.env"]

validation {
condition = alltrue([for pattern in var.excluded_files : trimspace(pattern) != ""])
error_message = "excluded_files entries must not be empty."
}
}

locals {
normalized_server_url = "${trim(var.server_url, "/")}/"
extension_spec = "${var.extension_id}@${var.extension_version}"
}

output "extensions" {
description = "Versioned extension identifiers to pass to a compatible web IDE module."
value = var.install_extension ? [local.extension_spec] : []
}

output "settings" {
description = "Open Collaboration Tools settings to merge into a compatible IDE module."
value = {
"oct.serverUrl" = local.normalized_server_url
"oct.alwaysAskToOverrideServerUrl" = var.always_ask_to_override_server_url
"oct.joinAcceptMode" = var.join_accept_mode
"oct.joinAllowlist" = var.join_allowlist
"oct.files.exclude" = var.excluded_files
}
}
Loading