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
9 changes: 9 additions & 0 deletions .icons/1claw.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added registry/kmjones1979/.images/avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions registry/kmjones1979/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
display_name: Kevin Jones
bio: Developer building modules for Coder workspaces
avatar: ./.images/avatar.png
github: kmjones1979
status: community
---

# Kevin Jones

Developer building modules for Coder workspaces.
74 changes: 74 additions & 0 deletions registry/kmjones1979/modules/oneclaw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
display_name: 1Claw
description: Vault-backed secrets and MCP server wiring for 1Claw in Coder workspaces
icon: ../../../../.icons/1claw.svg
verified: false
tags: [secrets, mcp, ai]
---

# 1Claw

Give every Coder workspace scoped access to [1Claw](https://1claw.xyz) so AI coding agents can read secrets from an encrypted vault instead of hardcoded credentials. The module merges a `streamable-http` MCP server entry into Cursor and Claude Code config files without overwriting other MCP servers.

Upstream source: [github.com/1clawAI/1claw-coder-workspace-module](https://github.com/1clawAI/1claw-coder-workspace-module).

## Usage

### Bootstrap mode (recommended)

Creates a vault, agent, and access policy on the first workspace boot using a human `1ck_` API key, then caches credentials in `~/.1claw/bootstrap.json` for subsequent starts.

```tf
module "oneclaw" {
source = "registry.coder.com/kmjones1979/oneclaw/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
human_api_key = var.oneclaw_human_key
}
```

#### Post-bootstrap cleanup (recommended)

The `1ck_` human key is a privileged credential that can create and destroy vaults in your 1Claw account. It is only needed the first time the workspace boots. After the initial bootstrap succeeds:

1. Clear the variable in your Terraform:

```tf
module "oneclaw" {
source = "registry.coder.com/kmjones1979/oneclaw/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
human_api_key = "" # scrubbed after first bootstrap
}
```

2. Re-apply the template. On the next workspace start, the script loads credentials from `~/.1claw/bootstrap.json` and no longer references the human key. The workspace continues to work with the scoped `ocv_` agent key only.

### Manual mode

Pre-provision the vault and agent out-of-band and pass only the scoped `ocv_` agent key. Recommended for production and for threat models that include untrusted code running inside the workspace.

```tf
module "oneclaw" {
source = "registry.coder.com/kmjones1979/oneclaw/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
vault_id = var.oneclaw_vault_id
api_token = var.oneclaw_agent_key
}
```

## Security notes

The module is written so that the `1ck_` human bootstrap key leaves no persistent trace in the workspace:

- The `ocv_` agent key exposed to the AI is scoped to a single vault and a single secret-path policy. That defines the blast radius of anything the AI does.
- The `1ck_` human key is injected into the bootstrap script as a sensitive `coder_env` variable (`_ONECLAW_HUMAN_API_KEY`), **never** templated into the script body. Because of this, it does **not** appear in `/tmp/coder-agent.log` (which records the rendered script) or in the Terraform state file's `coder_script` resource. The rendered script only contains the literal reference `HUMAN_KEY="${_ONECLAW_HUMAN_API_KEY:-}"`.
- During bootstrap, the human key is sent to the 1Claw API via `curl --data-binary @-` from stdin, so it never appears in process argv (`ps aux` / `/proc/<pid>/cmdline`).
- The key is scrubbed from shell variables (`unset HUMAN_KEY` / `unset _ONECLAW_HUMAN_API_KEY`) immediately after authentication, so downstream processes started by the script do not inherit it.
- The key is **never** written to `~/.1claw/bootstrap.json`, `~/.cursor/mcp.json`, `~/.config/claude/mcp.json`, or any other on-disk file. Only the scoped `ocv_` agent key and the vault id are persisted.
- For highest assurance, use manual mode with a pre-provisioned `ocv_` key so the `1ck_` key never reaches the workspace at all.

## Requirements

Bootstrap mode runs inside the workspace and requires `curl` and `python3` in the container image.
84 changes: 84 additions & 0 deletions registry/kmjones1979/modules/oneclaw/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
findResourceInstance,
} from "~test";

describe("oneclaw", async () => {
await runTerraformInit(import.meta.dir);

testRequiredVariables(import.meta.dir, {
agent_id: "test-agent",
});

it("manual mode sets env vars and run script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
api_token: "ocv_testtoken",
});

const vaultEnv = findResourceInstance(state, "coder_env", "vault_id");
expect(vaultEnv.name).toBe("ONECLAW_VAULT_ID");

const apiKeyEnv = findResourceInstance(state, "coder_env", "agent_api_key");
expect(apiKeyEnv.name).toBe("ONECLAW_AGENT_API_KEY");

const baseUrlEnv = findResourceInstance(state, "coder_env", "base_url");
expect(baseUrlEnv.name).toBe("ONECLAW_BASE_URL");
expect(baseUrlEnv.value).toBe("https://api.1claw.xyz");

const runScript = findResourceInstance(state, "coder_script", "run");
expect(runScript.display_name).toBe("1Claw");
expect(runScript.start_blocks_login).toBe(false);

const provisions = state.resources.filter(
(r) => r.type === "null_resource" && r.name === "provision",
);
expect(provisions.length).toBe(0);
});

it("bootstrap mode enables blocking run script and injects human key via coder_env", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
human_api_key: "1ck_test_human_key",
});

const runScript = findResourceInstance(state, "coder_script", "run");
expect(runScript.display_name).toBe("1Claw");
expect(runScript.start_blocks_login).toBe(true);

// The human key is delivered via coder_env (sensitive), NOT baked into the
// script body, so it never lands in the Coder agent's script log.
const humanKeyEnv = findResourceInstance(
state,
"coder_env",
"human_api_key",
);
expect(humanKeyEnv.name).toBe("_ONECLAW_HUMAN_API_KEY");

// And the actual key value must not appear anywhere in the rendered script text.
expect(runScript.script).not.toContain("1ck_test_human_key");
// The script must reference the env var, not a literal value.
expect(runScript.script).toContain("_ONECLAW_HUMAN_API_KEY");

const provisions = state.resources.filter(
(r) => r.type === "null_resource" && r.name === "provision",
);
expect(provisions.length).toBe(0);
});

it("custom base_url is reflected in env", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
api_token: "ocv_testtoken",
base_url: "https://api.example.com",
});

const baseUrlEnv = findResourceInstance(state, "coder_env", "base_url");
expect(baseUrlEnv.value).toBe("https://api.example.com");
});
});
223 changes: 223 additions & 0 deletions registry/kmjones1979/modules/oneclaw/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
terraform {
required_version = ">= 1.4"

required_providers {
coder = {
source = "coder/coder"
version = ">= 2.12"
}
}
}

variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}

variable "vault_id" {
type = string
description = "The 1Claw vault ID to scope MCP access to. Optional when using bootstrap mode (human_api_key)."
default = ""

validation {
condition = var.vault_id == "" || can(regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", var.vault_id))
error_message = "vault_id must be a valid UUID or empty (for bootstrap mode)."
}
}

variable "api_token" {
type = string
sensitive = true
description = "1Claw agent API key (starts with ocv_). Optional when using bootstrap mode (human_api_key)."
default = ""
}

variable "human_api_key" {
type = string
sensitive = true
default = ""
description = "One-time human 1ck_ API key for auto-provisioning. On first workspace start, creates a vault, agent, and policy automatically. Credentials are cached in ~/.1claw/bootstrap.json for subsequent starts."
}

variable "bootstrap_vault_name" {
type = string
default = "coder-workspace"
description = "Name for the auto-created vault (only used when vault_id is not provided and human_api_key is set)."
}

variable "bootstrap_agent_name" {
type = string
default = ""
description = "Name for the auto-created agent. Defaults to coder-<workspace_name>."
}

variable "bootstrap_policy_path" {
type = string
default = "**"
description = "Secret path pattern for the auto-created policy (glob). Defaults to all secrets."
}

variable "agent_id_1claw" {
type = string
description = "Optional 1Claw agent UUID. When omitted, the MCP server resolves the agent from the API key prefix."
default = ""
}

variable "mcp_host" {
type = string
description = "Base URL of the 1Claw MCP server."
default = "https://mcp.1claw.xyz/mcp"

validation {
condition = can(regex("^https?://", var.mcp_host))
error_message = "mcp_host must start with http:// or https://."
}
}

variable "base_url" {
type = string
description = "Base URL of the 1Claw Vault API (used by ONECLAW_BASE_URL env var)."
default = "https://api.1claw.xyz"

validation {
condition = can(regex("^https?://", var.base_url))
error_message = "base_url must start with http:// or https://."
}
}

variable "install_cursor_config" {
type = bool
description = "Whether to write MCP config to the Cursor IDE config path."
default = true
}

variable "install_claude_config" {
type = bool
description = "Whether to write MCP config to the Claude Code config path."
default = true
}

variable "cursor_config_path" {
type = string
description = "Path where the Cursor MCP config file is written."
default = "$HOME/.cursor/mcp.json"
}

variable "claude_config_path" {
type = string
description = "Path where the Claude Code MCP config file is written."
default = "$HOME/.config/claude/mcp.json"
}

variable "icon" {
type = string
description = "Icon to display for the setup script in the Coder UI."
default = "/icon/vault.svg"
}

variable "order" {
type = number
description = "The order determines the position of app in the UI presentation."
default = null
}

data "coder_workspace" "me" {}

data "coder_workspace_owner" "me" {}

locals {
bootstrap_mode = var.human_api_key != ""
bootstrap_agent_name = (
var.bootstrap_agent_name != "" ? var.bootstrap_agent_name :
"coder-${data.coder_workspace.me.name}"
)
}

resource "coder_env" "vault_id" {
count = var.vault_id != "" ? 1 : 0
agent_id = var.agent_id
name = "ONECLAW_VAULT_ID"
value = var.vault_id
}

resource "coder_env" "agent_api_key" {
count = var.api_token != "" ? 1 : 0
agent_id = var.agent_id
name = "ONECLAW_AGENT_API_KEY"
value = var.api_token
}

resource "coder_env" "oneclaw_agent_id" {
count = var.agent_id_1claw != "" ? 1 : 0
agent_id = var.agent_id
name = "ONECLAW_AGENT_ID"
value = var.agent_id_1claw
}

resource "coder_env" "base_url" {
agent_id = var.agent_id
name = "ONECLAW_BASE_URL"
value = var.base_url
}

# Sensitive values are passed via coder_env (not templated into the script body)
# so they don't appear in the Coder agent's script log. The agent log is 0600 on
# the coder user, but that's the same user the AI runs as in most images, so we
# want to avoid any on-disk copy of the 1ck_ key in the workspace.
resource "coder_env" "human_api_key" {
count = local.bootstrap_mode ? 1 : 0
agent_id = var.agent_id
name = "_ONECLAW_HUMAN_API_KEY"
value = var.human_api_key
}

resource "coder_script" "run" {
agent_id = var.agent_id
display_name = "1Claw"
icon = var.icon
run_on_start = true
start_blocks_login = local.bootstrap_mode

script = templatefile("${path.module}/scripts/run.sh", {
BOOTSTRAP_MODE = local.bootstrap_mode ? "true" : "false"
BASE_URL = var.base_url
VAULT_ID_INPUT = var.vault_id
VAULT_NAME = var.bootstrap_vault_name
AGENT_NAME = local.bootstrap_agent_name
POLICY_PATH = var.bootstrap_policy_path
STATE_DIR = "$HOME/.1claw"
MCP_HOST = var.mcp_host
INSTALL_CURSOR_CONFIG = var.install_cursor_config ? "true" : "false"
INSTALL_CLAUDE_CONFIG = var.install_claude_config ? "true" : "false"
CURSOR_CONFIG_PATH = var.cursor_config_path
CLAUDE_CONFIG_PATH = var.claude_config_path
})
}

output "mcp_config_path" {
description = "Primary MCP config file path (Cursor). Use this to reference the config from downstream resources."
value = var.cursor_config_path
}

output "claude_config_path" {
description = "Claude Code MCP config file path."
value = var.install_claude_config ? var.claude_config_path : ""
}

output "vault_id" {
description = "The 1Claw vault ID configured for this workspace (manual mode only; bootstrap mode resolves the vault ID inside the workspace)."
value = var.vault_id
sensitive = true
}

output "agent_id_1claw" {
description = "The 1Claw agent UUID, if provided via variable."
value = var.agent_id_1claw
sensitive = true
}

output "provisioning_mode" {
description = "Which provisioning mode is active: bootstrap or manual."
value = local.bootstrap_mode ? "bootstrap" : "manual"
sensitive = true
}
Loading