From f8e8b0eae35707602453d4b9be8598263ed69047 Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:34:18 -0500 Subject: [PATCH 1/2] feat: add oneclaw module for 1Claw MCP and vault integration Add kmjones1979 namespace and oneclaw module, ported from 1clawAI/1claw-coder-workspace-module. Provides vault-backed secrets and MCP server config for AI coding agents in Coder workspaces. - Namespace: kmjones1979 (avatar from GitHub) - Module: oneclaw with three provisioning modes (terraform-native, shell bootstrap, manual) - Tests: main.tftest.hcl (5 runs) and main.test.ts (5 tests) - Scripts: provision.sh, bootstrap.sh, setup.sh Made-with: Cursor --- registry/kmjones1979/.images/avatar.png | Bin 0 -> 16196 bytes registry/kmjones1979/README.md | 11 + .../kmjones1979/modules/oneclaw/README.md | 61 +++++ .../kmjones1979/modules/oneclaw/main.test.ts | 97 ++++++++ registry/kmjones1979/modules/oneclaw/main.tf | 216 ++++++++++++++++++ .../modules/oneclaw/main.tftest.hcl | 103 +++++++++ .../kmjones1979/modules/oneclaw/outputs.tf | 33 +++ .../modules/oneclaw/scripts/bootstrap.sh | 151 ++++++++++++ .../modules/oneclaw/scripts/provision.sh | 151 ++++++++++++ .../modules/oneclaw/scripts/setup.sh | 124 ++++++++++ .../kmjones1979/modules/oneclaw/variables.tf | 153 +++++++++++++ 11 files changed, 1100 insertions(+) create mode 100644 registry/kmjones1979/.images/avatar.png create mode 100644 registry/kmjones1979/README.md create mode 100644 registry/kmjones1979/modules/oneclaw/README.md create mode 100644 registry/kmjones1979/modules/oneclaw/main.test.ts create mode 100644 registry/kmjones1979/modules/oneclaw/main.tf create mode 100644 registry/kmjones1979/modules/oneclaw/main.tftest.hcl create mode 100644 registry/kmjones1979/modules/oneclaw/outputs.tf create mode 100644 registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh create mode 100755 registry/kmjones1979/modules/oneclaw/scripts/provision.sh create mode 100644 registry/kmjones1979/modules/oneclaw/scripts/setup.sh create mode 100644 registry/kmjones1979/modules/oneclaw/variables.tf diff --git a/registry/kmjones1979/.images/avatar.png b/registry/kmjones1979/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..dd7f47e6290e77e7eab1335b7964a8c9b8231245 GIT binary patch literal 16196 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 literal 0 HcmV?d00001 diff --git a/registry/kmjones1979/README.md b/registry/kmjones1979/README.md new file mode 100644 index 000000000..5d0510782 --- /dev/null +++ b/registry/kmjones1979/README.md @@ -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. diff --git a/registry/kmjones1979/modules/oneclaw/README.md b/registry/kmjones1979/modules/oneclaw/README.md new file mode 100644 index 000000000..c0e3cde2e --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/README.md @@ -0,0 +1,61 @@ +--- +display_name: 1Claw +description: Vault-backed secrets and MCP server wiring for 1Claw in Coder workspaces +icon: ../../../../.icons/vault.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 supports three provisioning modes — Terraform-native, shell bootstrap, and manual — and 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 + +### Terraform-native mode (recommended) + +Provisions vault, agent, and access policy at `terraform apply`; cleans up on `terraform destroy`. + +```tf +module "oneclaw" { + source = "registry.coder.com/kmjones1979/oneclaw/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + master_api_key = var.oneclaw_key +} +``` + +### Manual mode + +Use an existing vault and agent API key from the 1Claw dashboard. + +```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 +} +``` + +### Shell bootstrap mode + +Creates vault and agent on the first workspace boot, then caches credentials 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 +} +``` + +> [!NOTE] +> **Terraform-native mode** runs a `local-exec` provisioner on the machine executing Terraform. It needs network access to the 1Claw API, `curl`, and `python3`. + +> [!TIP] +> Combine this module with other registry modules (e.g. Cursor or Claude Code). The MCP setup script merges into existing `mcp.json` files instead of replacing them. diff --git a/registry/kmjones1979/modules/oneclaw/main.test.ts b/registry/kmjones1979/modules/oneclaw/main.test.ts new file mode 100644 index 000000000..89e03d8e8 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.test.ts @@ -0,0 +1,97 @@ +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 mcp 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", + "oneclaw_vault_id", + ); + expect(vaultEnv.name).toBe("ONECLAW_VAULT_ID"); + + const apiKeyEnv = findResourceInstance( + state, + "coder_env", + "oneclaw_agent_api_key", + ); + expect(apiKeyEnv.name).toBe("ONECLAW_AGENT_API_KEY"); + + const baseUrlEnv = findResourceInstance( + state, + "coder_env", + "oneclaw_base_url", + ); + expect(baseUrlEnv.name).toBe("ONECLAW_BASE_URL"); + expect(baseUrlEnv.value).toBe("https://api.1claw.xyz"); + + const mcpScript = findResourceInstance( + state, + "coder_script", + "oneclaw_mcp_setup", + ); + expect(mcpScript.display_name).toBe("1Claw MCP Setup"); + + const bootstrapScripts = state.resources.filter( + (r) => r.type === "coder_script" && r.name === "oneclaw_bootstrap", + ); + expect(bootstrapScripts.length).toBe(0); + + const provisions = state.resources.filter( + (r) => r.type === "null_resource" && r.name === "oneclaw_provision", + ); + expect(provisions.length).toBe(0); + }); + + it("bootstrap mode creates bootstrap script", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + human_api_key: "1ck_test_human_key", + }); + + const bootstrap = findResourceInstance( + state, + "coder_script", + "oneclaw_bootstrap", + ); + expect(bootstrap.display_name).toBe("1Claw Bootstrap"); + + const provisions = state.resources.filter( + (r) => r.type === "null_resource" && r.name === "oneclaw_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", + "oneclaw_base_url", + ); + expect(baseUrlEnv.value).toBe("https://api.example.com"); + }); +}); diff --git a/registry/kmjones1979/modules/oneclaw/main.tf b/registry/kmjones1979/modules/oneclaw/main.tf new file mode 100644 index 000000000..3dbabfa98 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.tf @@ -0,0 +1,216 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.12" + } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } + } +} + +locals { + # Which mode are we in? + tf_native_mode = var.master_api_key != "" + bootstrap_mode = var.human_api_key != "" && !local.tf_native_mode + manual_mode = !local.tf_native_mode && !local.bootstrap_mode + + provision_state_file = "${path.module}/.provision-state.json" + + provision_vault_name = ( + var.provision_vault_name != "" ? var.provision_vault_name : + "coder-${data.coder_workspace.me.name}" + ) + provision_agent_name = ( + var.provision_agent_name != "" ? var.provision_agent_name : + "coder-${data.coder_workspace.me.name}-agent" + ) + + # Resolve effective vault_id and api_token. + # In TF-native mode these come from the provision state file after null_resource runs. + effective_vault_id = local.tf_native_mode ? local.provisioned_vault_id : var.vault_id + effective_token = local.tf_native_mode ? local.provisioned_token : var.api_token + + # Read provision state (only meaningful after null_resource.oneclaw_provision has run). + provision_state = local.tf_native_mode && fileexists(local.provision_state_file) ? jsondecode(file(local.provision_state_file)) : {} + + provisioned_vault_id = lookup(local.provision_state, "vault_id", "") + provisioned_token = lookup(local.provision_state, "agent_api_key", "") + provisioned_agent_id = lookup(local.provision_state, "agent_id", "") +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +# =========================================================================== +# Terraform-native provisioning (apply-time create, destroy-time cleanup) +# =========================================================================== + +resource "null_resource" "oneclaw_provision" { + count = local.tf_native_mode ? 1 : 0 + + # All values needed at destroy time must live in triggers (Terraform restriction). + triggers = { + workspace_id = data.coder_workspace.me.id + workspace_name = data.coder_workspace.me.name + vault_name = local.provision_vault_name + agent_name = local.provision_agent_name + state_file = local.provision_state_file + base_url = var.base_url + master_api_key = var.master_api_key + destroy_vault = tostring(var.auto_destroy_vault) + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = templatefile("${path.module}/scripts/provision.sh", { + BASE_URL = var.base_url + MASTER_API_KEY = var.master_api_key + WORKSPACE_ID = data.coder_workspace.me.id + WORKSPACE_NAME = data.coder_workspace.me.name + VAULT_NAME = local.provision_vault_name + AGENT_NAME = local.provision_agent_name + POLICY_PATH = var.provision_policy_path + TOKEN_TTL_SECONDS = tostring(var.token_ttl_hours * 3600) + STATE_FILE = local.provision_state_file + }) + } + + provisioner "local-exec" { + when = destroy + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + STATE_FILE="${self.triggers.state_file}" + API_URL="${self.triggers.base_url}" + MASTER_KEY="${self.triggers.master_api_key}" + DESTROY_VAULT="${self.triggers.destroy_vault}" + + if [ ! -f "$STATE_FILE" ]; then + echo "[1claw-deprovision] No state file — nothing to clean up" + exit 0 + fi + + VAULT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['vault_id'])") + AGENT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['agent_id'])") + echo "[1claw-deprovision] Agent: $AGENT_ID Vault: $VAULT_ID" + + # Authenticate + AUTH=$(curl -sf -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -d "{\"api_key\": \"$MASTER_KEY\"}" \ + "$API_URL/v1/auth/api-key-token" 2>&1) || { + echo "[1claw-deprovision] WARN: Auth failed — manual cleanup needed" + rm -f "$STATE_FILE"; exit 0 + } + AUTH_HTTP=$(echo "$AUTH" | tail -1) + AUTH_BODY=$(echo "$AUTH" | sed '$d') + if [ "$(echo "$AUTH_HTTP" | head -c1)" != "2" ]; then + echo "[1claw-deprovision] WARN: Auth HTTP $AUTH_HTTP — manual cleanup needed" + rm -f "$STATE_FILE"; exit 0 + fi + JWT=$(python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])" <<< "$AUTH_BODY") + + # Delete agent + echo "[1claw-deprovision] Deleting agent $AGENT_ID..." + curl -sf -X DELETE -H "Authorization: Bearer $JWT" "$API_URL/v1/agents/$AGENT_ID" >/dev/null 2>&1 \ + && echo "[1claw-deprovision] Agent deleted" \ + || echo "[1claw-deprovision] WARN: Agent delete failed (may already be gone)" + + # Optionally delete vault + if [ "$DESTROY_VAULT" = "true" ]; then + echo "[1claw-deprovision] Deleting vault $VAULT_ID..." + curl -sf -X DELETE -H "Authorization: Bearer $JWT" "$API_URL/v1/vaults/$VAULT_ID" >/dev/null 2>&1 \ + && echo "[1claw-deprovision] Vault deleted" \ + || echo "[1claw-deprovision] WARN: Vault delete failed (may have secrets or already be gone)" + else + echo "[1claw-deprovision] Vault $VAULT_ID retained (set auto_destroy_vault = true to delete)" + fi + + rm -f "$STATE_FILE" + echo "[1claw-deprovision] Cleanup complete" + EOT + } +} + +# =========================================================================== +# Environment variables (injected into the workspace agent) +# =========================================================================== + +resource "coder_env" "oneclaw_vault_id" { + count = local.effective_vault_id != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_VAULT_ID" + value = local.effective_vault_id +} + +resource "coder_env" "oneclaw_agent_api_key" { + count = local.effective_token != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_AGENT_API_KEY" + value = local.effective_token +} + +resource "coder_env" "oneclaw_agent_id" { + count = var.agent_id_1claw != "" || local.provisioned_agent_id != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_AGENT_ID" + value = var.agent_id_1claw != "" ? var.agent_id_1claw : local.provisioned_agent_id +} + +resource "coder_env" "oneclaw_base_url" { + agent_id = var.agent_id + name = "ONECLAW_BASE_URL" + value = var.base_url +} + +# =========================================================================== +# Shell bootstrap (optional, first-run provisioning inside the workspace) +# =========================================================================== + +resource "coder_script" "oneclaw_bootstrap" { + count = local.bootstrap_mode ? 1 : 0 + agent_id = var.agent_id + display_name = "1Claw Bootstrap" + icon = var.icon + run_on_start = true + start_blocks_login = true + + script = templatefile("${path.module}/scripts/bootstrap.sh", { + HUMAN_API_KEY = var.human_api_key + BASE_URL = var.base_url + VAULT_ID = var.vault_id + VAULT_NAME = var.bootstrap_vault_name + AGENT_NAME = var.bootstrap_agent_name != "" ? var.bootstrap_agent_name : "coder-${data.coder_workspace.me.name}" + POLICY_PATH = var.bootstrap_policy_path + STATE_DIR = "$HOME/.1claw" + }) +} + +# =========================================================================== +# MCP config file injection +# =========================================================================== + +resource "coder_script" "oneclaw_mcp_setup" { + agent_id = var.agent_id + display_name = "1Claw MCP Setup" + icon = var.icon + run_on_start = true + start_blocks_login = false + + script = templatefile("${path.module}/scripts/setup.sh", { + MCP_HOST = var.mcp_host + VAULT_ID = local.effective_vault_id + API_TOKEN = local.effective_token + BOOTSTRAP_MODE = local.bootstrap_mode ? "true" : "false" + INSTALL_CURSOR_CONFIG = var.install_cursor_config + INSTALL_CLAUDE_CONFIG = var.install_claude_config + CURSOR_CONFIG_PATH = var.cursor_config_path + CLAUDE_CONFIG_PATH = var.claude_config_path + }) +} diff --git a/registry/kmjones1979/modules/oneclaw/main.tftest.hcl b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl new file mode 100644 index 000000000..9c8ee927a --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl @@ -0,0 +1,103 @@ +run "manual_mode" { + command = plan + + variables { + agent_id = "test-agent-manual" + vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + api_token = "ocv_testtoken" + } + + assert { + condition = length(coder_env.oneclaw_vault_id) == 1 + error_message = "ONECLAW_VAULT_ID should be set in manual mode" + } + + assert { + condition = length(coder_env.oneclaw_agent_api_key) == 1 + error_message = "ONECLAW_AGENT_API_KEY should be set in manual mode" + } + + assert { + condition = length(null_resource.oneclaw_provision) == 0 + error_message = "No provision resource in manual mode" + } + + assert { + condition = length(coder_script.oneclaw_bootstrap) == 0 + error_message = "No bootstrap script in manual mode" + } +} + +run "terraform_native_mode" { + command = plan + + variables { + agent_id = "test-agent-tf" + master_api_key = "1ck_test_master_key" + } + + assert { + condition = length(null_resource.oneclaw_provision) == 1 + error_message = "Terraform-native mode should create the provision null_resource" + } + + assert { + condition = length(coder_script.oneclaw_bootstrap) == 0 + error_message = "No bootstrap script in terraform-native mode" + } +} + +run "bootstrap_mode" { + command = plan + + variables { + agent_id = "test-agent-bootstrap" + human_api_key = "1ck_test_human_key" + } + + assert { + condition = length(coder_script.oneclaw_bootstrap) == 1 + error_message = "Bootstrap mode should create the bootstrap script" + } + + assert { + condition = length(null_resource.oneclaw_provision) == 0 + error_message = "No provision resource in bootstrap mode" + } +} + +run "master_key_takes_precedence_over_human" { + command = plan + + variables { + agent_id = "test-agent-priority" + master_api_key = "1ck_master" + human_api_key = "1ck_human" + } + + assert { + condition = length(null_resource.oneclaw_provision) == 1 + error_message = "master_api_key should win when both keys are set" + } + + assert { + condition = length(coder_script.oneclaw_bootstrap) == 0 + error_message = "No bootstrap script when master_api_key is set" + } +} + +run "custom_base_url" { + command = plan + + variables { + agent_id = "test-agent-mcp" + vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + api_token = "ocv_testtoken" + base_url = "https://api.example.com" + } + + assert { + condition = coder_env.oneclaw_base_url.value == "https://api.example.com" + error_message = "ONECLAW_BASE_URL should match base_url" + } +} diff --git a/registry/kmjones1979/modules/oneclaw/outputs.tf b/registry/kmjones1979/modules/oneclaw/outputs.tf new file mode 100644 index 000000000..f106b092a --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/outputs.tf @@ -0,0 +1,33 @@ +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." + value = local.effective_vault_id + sensitive = true +} + +output "scoped_token" { + description = "The agent API key (ocv_) for this workspace. Only populated in Terraform-native mode." + value = local.provisioned_token + sensitive = true +} + +output "agent_id_1claw" { + description = "The 1Claw agent UUID provisioned for this workspace." + value = local.provisioned_agent_id != "" ? local.provisioned_agent_id : var.agent_id_1claw + sensitive = true +} + +output "provisioning_mode" { + description = "Which provisioning mode is active: terraform_native, bootstrap, or manual." + value = local.tf_native_mode ? "terraform_native" : (local.bootstrap_mode ? "bootstrap" : "manual") + sensitive = true +} diff --git a/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh b/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh new file mode 100644 index 000000000..0faeeabaa --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh @@ -0,0 +1,151 @@ +#!/bin/bash +set -euo pipefail + +LOG_PREFIX="[1claw-bootstrap]" + +log() { + echo "$LOG_PREFIX $*" +} + +die() { + log "ERROR: $*" >&2 + exit 1 +} + +STATE_DIR=$(eval echo "${STATE_DIR}") +STATE_FILE="$STATE_DIR/bootstrap.json" +HUMAN_KEY="${HUMAN_API_KEY}" +API_URL="${BASE_URL}" +VAULT="${VAULT_ID}" +VAULT_NAME_IN="${VAULT_NAME}" +AGENT_NAME_IN="${AGENT_NAME}" +POLICY_PATH_IN="${POLICY_PATH}" + +# --- Early exit if already bootstrapped --- +if [ -f "$STATE_FILE" ]; then + log "Bootstrap state found at $STATE_FILE — skipping provisioning" + exit 0 +fi + +if [ -z "$HUMAN_KEY" ]; then + die "human_api_key is required for bootstrap mode" +fi + +api_call() { + local method="$1" + local path="$2" + local token="$3" + local body="$${4:-}" + + local response + response=$(curl -s -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + $${body:+-d "$body"} \ + -X "$method" "$API_URL$path" 2>&1) || { + log "API call failed: $method $path" + log "Response: $response" + return 1 + } + + local http_code + http_code=$(echo "$response" | tail -1) + local body_out + body_out=$(echo "$response" | sed '$d') + + if [ "$${http_code:0:1}" != "2" ]; then + log "API error: $method $path returned HTTP $http_code" + log "Response: $body_out" + return 1 + fi + + echo "$body_out" +} + +json_get() { + python3 -c "import json,sys; print(json.load(sys.stdin)$1)" +} + +# --- Step 1: Exchange human API key for JWT --- +log "Authenticating with 1Claw API..." +AUTH_RESPONSE=$(curl -s -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -d "{\"api_key\": \"$HUMAN_KEY\"}" \ + "$API_URL/v1/auth/api-key-token" 2>&1) || die "Failed to authenticate with human API key" + +AUTH_HTTP=$(echo "$AUTH_RESPONSE" | tail -1) +AUTH_BODY=$(echo "$AUTH_RESPONSE" | sed '$d') + +if [ "$${AUTH_HTTP:0:1}" != "2" ]; then + die "Authentication failed (HTTP $AUTH_HTTP): $AUTH_BODY" +fi + +JWT=$(echo "$AUTH_BODY" | json_get "['access_token']") +log "Authenticated successfully" + +# --- Step 2: Resolve or create vault --- +if [ -n "$VAULT" ]; then + log "Using provided vault: $VAULT" +else + log "Creating vault '$VAULT_NAME_IN'..." + VAULT_RESPONSE=$(api_call POST "/v1/vaults" "$JWT" \ + "{\"name\": \"$VAULT_NAME_IN\"}") || { + log "Vault creation failed — looking for existing vault named '$VAULT_NAME_IN'" + VAULTS_RESPONSE=$(api_call GET "/v1/vaults" "$JWT") || die "Failed to list vaults" + VAULT=$(echo "$VAULTS_RESPONSE" | python3 -c " +import json, sys +vaults = json.load(sys.stdin).get('vaults', []) +for v in vaults: + if v['name'] == '$VAULT_NAME_IN': + print(v['id']) + sys.exit(0) +sys.exit(1) +") || die "Could not find existing vault named '$VAULT_NAME_IN'" + log "Found existing vault: $VAULT" + } + if [ -z "$VAULT" ]; then + VAULT=$(echo "$VAULT_RESPONSE" | json_get "['id']") + log "Created vault: $VAULT" + fi +fi + +# --- Step 3: Create agent --- +log "Creating agent '$AGENT_NAME_IN'..." +AGENT_RESPONSE=$(api_call POST "/v1/agents" "$JWT" \ + "{\"name\": \"$AGENT_NAME_IN\", \"vault_ids\": [\"$VAULT\"]}") || die "Failed to create agent" + +AGENT_ID=$(echo "$AGENT_RESPONSE" | json_get "['agent']['id']") +AGENT_API_KEY=$(echo "$AGENT_RESPONSE" | json_get "['api_key']") + +if [ -z "$AGENT_API_KEY" ] || [ "$AGENT_API_KEY" = "None" ]; then + die "Agent created but no API key returned — check auth_method" +fi +log "Created agent: $AGENT_ID" + +# --- Step 4: Create access policy --- +log "Creating access policy (path: $POLICY_PATH_IN)..." +api_call POST "/v1/vaults/$VAULT/policies" "$JWT" \ + "{\"secret_path_pattern\": \"$POLICY_PATH_IN\", \"principal_type\": \"agent\", \"principal_id\": \"$AGENT_ID\", \"permissions\": [\"read\", \"write\"]}" \ + > /dev/null || die "Failed to create policy" +log "Policy created — agent can access $POLICY_PATH_IN" + +# --- Step 5: Save state --- +mkdir -p "$STATE_DIR" + +python3 - "$STATE_FILE" "$VAULT" "$AGENT_ID" "$AGENT_API_KEY" << 'PYEOF' +import json, sys +state = { + "vault_id": sys.argv[2], + "agent_id": sys.argv[3], + "agent_api_key": sys.argv[4] +} +with open(sys.argv[1], "w") as f: + json.dump(state, f, indent=2) +PYEOF + +chmod 600 "$STATE_FILE" + +log "Bootstrap complete — credentials saved to $STATE_FILE" +log " Vault ID: $VAULT" +log " Agent ID: $AGENT_ID" +log " Agent key: $${AGENT_API_KEY:0:12}..." diff --git a/registry/kmjones1979/modules/oneclaw/scripts/provision.sh b/registry/kmjones1979/modules/oneclaw/scripts/provision.sh new file mode 100755 index 000000000..893b7afff --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/scripts/provision.sh @@ -0,0 +1,151 @@ +#!/bin/bash +set -euo pipefail + +LOG_PREFIX="[1claw-provision]" +log() { echo "$LOG_PREFIX $*"; } +die() { + log "ERROR: $*" >&2 + exit 1 +} + +API_URL="${BASE_URL}" +MASTER_KEY="${MASTER_API_KEY}" +WORKSPACE_ID="${WORKSPACE_ID}" +WORKSPACE_NAME="${WORKSPACE_NAME}" +VAULT_NAME="${VAULT_NAME}" +AGENT_NAME="${AGENT_NAME}" +POLICY_PATH="${POLICY_PATH}" +TOKEN_TTL_SECS="${TOKEN_TTL_SECONDS}" +STATE_FILE="${STATE_FILE}" + +[ -n "$MASTER_KEY" ] || die "master_api_key is required" + +if [ -f "$STATE_FILE" ]; then + log "Provision state already exists at $STATE_FILE — skipping" + exit 0 +fi + +api_call() { + local method="$1" path="$2" token="$3" body="$${4:-}" + local response http_code body_out + + response=$(curl -sf -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + $${body:+-d "$body"} \ + -X "$method" "$API_URL$path" 2>&1) || { + log "curl failed: $method $path" + return 1 + } + + http_code=$(echo "$response" | tail -1) + body_out=$(echo "$response" | sed '$d') + + if [ "$${http_code:0:1}" != "2" ]; then + log "API $method $path => HTTP $http_code" + log "Body: $body_out" + return 1 + fi + echo "$body_out" +} + +json_get() { python3 -c "import json,sys; print(json.load(sys.stdin)$1)"; } + +# --- Step 1: Exchange master key for JWT --- +log "Authenticating..." +AUTH=$(curl -sf -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -d "{\"api_key\": \"$MASTER_KEY\"}" \ + "$API_URL/v1/auth/api-key-token" 2>&1) || die "Auth request failed" + +AUTH_HTTP=$(echo "$AUTH" | tail -1) +AUTH_BODY=$(echo "$AUTH" | sed '$d') +[ "$${AUTH_HTTP:0:1}" = "2" ] || die "Auth failed (HTTP $AUTH_HTTP): $AUTH_BODY" + +JWT=$(echo "$AUTH_BODY" | json_get "['access_token']") +log "Authenticated" + +# --- Step 2: Resolve or create vault --- +log "Creating vault '$VAULT_NAME'..." +VAULT_ID="" +VAULT_RESP=$(api_call POST "/v1/vaults" "$JWT" \ + "{\"name\": \"$VAULT_NAME\", \"description\": \"Auto-provisioned for Coder workspace $WORKSPACE_NAME ($WORKSPACE_ID)\"}") && { + VAULT_ID=$(echo "$VAULT_RESP" | json_get "['id']") + log "Created vault: $VAULT_ID" +} || { + log "Vault creation failed — searching for existing '$VAULT_NAME'" + LIST_RESP=$(api_call GET "/v1/vaults" "$JWT") || die "Cannot list vaults" + VAULT_ID=$(echo "$LIST_RESP" | python3 -c " +import json, sys +for v in json.load(sys.stdin).get('vaults', []): + if v['name'] == '$VAULT_NAME': + print(v['id']); sys.exit(0) +sys.exit(1) +") || die "No vault named '$VAULT_NAME' found" + log "Using existing vault: $VAULT_ID" +} + +# --- Step 3: Create agent scoped to this vault --- +AGENT_PAYLOAD=$(python3 -c " +import json, sys +payload = { + 'name': '$AGENT_NAME', + 'vault_ids': ['$VAULT_ID'], + 'description': 'Coder workspace $WORKSPACE_NAME ($WORKSPACE_ID)' +} +ttl = int('$TOKEN_TTL_SECS') if '$TOKEN_TTL_SECS' and '$TOKEN_TTL_SECS' != '0' else None +if ttl: + payload['token_ttl_seconds'] = ttl +print(json.dumps(payload)) +") + +log "Creating agent '$AGENT_NAME' (ttl=$${TOKEN_TTL_SECS}s)..." +AGENT_RESP=$(api_call POST "/v1/agents" "$JWT" "$AGENT_PAYLOAD") || die "Failed to create agent" + +AGENT_ID=$(echo "$AGENT_RESP" | json_get "['agent']['id']") +AGENT_KEY=$(echo "$AGENT_RESP" | json_get "['api_key']") + +[ -n "$AGENT_KEY" ] && [ "$AGENT_KEY" != "None" ] || die "Agent created but no API key returned" +log "Created agent: $AGENT_ID" + +# --- Step 4: Create access policy --- +log "Creating policy (path: $POLICY_PATH)..." +api_call POST "/v1/vaults/$VAULT_ID/policies" "$JWT" \ + "{\"secret_path_pattern\": \"$POLICY_PATH\", \"principal_type\": \"agent\", \"principal_id\": \"$AGENT_ID\", \"permissions\": [\"read\", \"write\"]}" \ + > /dev/null || die "Failed to create policy" +log "Policy created" + +# --- Step 5: Exchange agent key for a scoped JWT --- +log "Exchanging agent key for scoped token..." +TOKEN_RESP=$(curl -sf -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + -d "{\"agent_id\": \"$AGENT_ID\", \"api_key\": \"$AGENT_KEY\"}" \ + "$API_URL/v1/auth/agent-token" 2>&1) || die "Token exchange failed" + +TOKEN_HTTP=$(echo "$TOKEN_RESP" | tail -1) +TOKEN_BODY=$(echo "$TOKEN_RESP" | sed '$d') +[ "$${TOKEN_HTTP:0:1}" = "2" ] || die "Token exchange failed (HTTP $TOKEN_HTTP)" + +SCOPED_TOKEN=$(echo "$TOKEN_BODY" | json_get "['access_token']") +log "Got scoped token" + +# --- Step 6: Write state file --- +mkdir -p "$(dirname "$STATE_FILE")" +python3 - "$STATE_FILE" "$VAULT_ID" "$AGENT_ID" "$AGENT_KEY" "$SCOPED_TOKEN" "$WORKSPACE_ID" << 'PYEOF' +import json, sys +state = { + "vault_id": sys.argv[2], + "agent_id": sys.argv[3], + "agent_api_key": sys.argv[4], + "scoped_token": sys.argv[5], + "workspace_id": sys.argv[6] +} +with open(sys.argv[1], "w") as f: + json.dump(state, f, indent=2) +PYEOF +chmod 600 "$STATE_FILE" + +log "Provision complete" +log " Vault: $VAULT_ID" +log " Agent: $AGENT_ID" +log " Key: $${AGENT_KEY:0:12}..." diff --git a/registry/kmjones1979/modules/oneclaw/scripts/setup.sh b/registry/kmjones1979/modules/oneclaw/scripts/setup.sh new file mode 100644 index 000000000..3286531c8 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/scripts/setup.sh @@ -0,0 +1,124 @@ +#!/bin/bash +set -euo pipefail + +LOG_PREFIX="[1claw-mcp]" + +log() { + echo "$LOG_PREFIX $*" +} + +API_TOKEN="${API_TOKEN}" +VAULT_ID="${VAULT_ID}" + +# In bootstrap mode, API_TOKEN and VAULT_ID are empty at templatefile time. +# Wait for bootstrap.sh to produce the state file (scripts run concurrently). +BOOTSTRAP_MODE="${BOOTSTRAP_MODE}" +STATE_FILE="$HOME/.1claw/bootstrap.json" +if [ -z "$API_TOKEN" ] && [ "$BOOTSTRAP_MODE" = "true" ]; then + WAIT_SECS=0 + while [ ! -f "$STATE_FILE" ] && [ "$WAIT_SECS" -lt 120 ]; do + log "Waiting for bootstrap to complete ($WAIT_SECS/120s)..." + sleep 3 + WAIT_SECS=$((WAIT_SECS + 3)) + done +fi + +if [ -z "$API_TOKEN" ] && [ -f "$STATE_FILE" ]; then + log "Loading credentials from bootstrap state" + API_TOKEN=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['agent_api_key'])") + VAULT_ID=$(python3 -c "import json; print(json.load(open('$STATE_FILE'))['vault_id'])") +fi + +if [ -z "$API_TOKEN" ] || [ -z "$VAULT_ID" ]; then + log "WARNING: No API token or vault ID available — skipping MCP config" + log "Provide api_token + vault_id, or use human_api_key for bootstrap mode" + exit 0 +fi + +# Build the MCP config JSON via python3 for safe handling of special characters. +MCP_CONFIG=$( + python3 - "$API_TOKEN" "$VAULT_ID" << 'PYEOF' +import json, sys +config = { + "mcpServers": { + "1claw": { + "url": "${MCP_HOST}", + "headers": { + "Authorization": "Bearer " + sys.argv[1], + "X-Vault-ID": sys.argv[2] + } + } + } +} +print(json.dumps(config, indent=2)) +PYEOF +) + +# Write MCP_CONFIG to a temp file so the merge script can read it safely. +MCP_CONFIG_TMP=$(mktemp) +trap 'rm -f "$MCP_CONFIG_TMP"' EXIT +echo "$MCP_CONFIG" > "$MCP_CONFIG_TMP" + +write_config() { + local target_path="$1" + local label="$2" + + # Expand $HOME in the path + target_path=$(eval echo "$target_path") + + local target_dir + target_dir=$(dirname "$target_path") + + if [ ! -d "$target_dir" ]; then + log "Creating directory $target_dir for $label config" + mkdir -p "$target_dir" + fi + + if [ -f "$target_path" ]; then + log "Merging 1Claw MCP server into existing $label config at $target_path" + if command -v python3 &> /dev/null; then + python3 - "$target_path" "$MCP_CONFIG_TMP" << 'PYEOF' +import json, sys + +target_path = sys.argv[1] +new_config_path = sys.argv[2] + +existing = {} +try: + with open(target_path) as f: + existing = json.load(f) +except (json.JSONDecodeError, FileNotFoundError): + pass + +with open(new_config_path) as f: + new_server = json.load(f) + +existing.setdefault("mcpServers", {}).update(new_server.get("mcpServers", {})) + +with open(target_path, "w") as f: + json.dump(existing, f, indent=2) +PYEOF + else + log "python3 not found — overwriting $target_path" + cat "$MCP_CONFIG_TMP" > "$target_path" + fi + else + log "Writing $label MCP config to $target_path" + cat "$MCP_CONFIG_TMP" > "$target_path" + fi + + chmod 600 "$target_path" + log "$label MCP config ready at $target_path" +} + +# Cursor IDE config +if [ "${INSTALL_CURSOR_CONFIG}" = "true" ]; then + write_config "${CURSOR_CONFIG_PATH}" "Cursor" +fi + +# Claude Code config +if [ "${INSTALL_CLAUDE_CONFIG}" = "true" ]; then + write_config "${CLAUDE_CONFIG_PATH}" "Claude Code" +fi + +log "1Claw MCP setup complete" diff --git a/registry/kmjones1979/modules/oneclaw/variables.tf b/registry/kmjones1979/modules/oneclaw/variables.tf new file mode 100644 index 000000000..564b902d7 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/variables.tf @@ -0,0 +1,153 @@ +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-." +} + +variable "bootstrap_policy_path" { + type = string + default = "**" + description = "Secret path pattern for the auto-created policy (glob). Defaults to all secrets." +} + +variable "master_api_key" { + type = string + sensitive = true + default = "" + description = "Human 1ck_ API key for Terraform-native provisioning. Creates vault + agent at terraform apply; cleans up at terraform destroy. Credentials are available as outputs immediately — no shell bootstrap needed." +} + +variable "token_ttl_hours" { + type = number + default = 8 + description = "TTL in hours for the agent's scoped JWT (Terraform-native mode). Set to 0 for the platform default (1 hour)." + + validation { + condition = var.token_ttl_hours >= 0 && var.token_ttl_hours <= 720 + error_message = "token_ttl_hours must be between 0 and 720 (30 days)." + } +} + +variable "auto_destroy_vault" { + type = bool + default = false + description = "Whether to delete the provisioned vault on terraform destroy. When false (default), only the agent is deleted." +} + +variable "provision_vault_name" { + type = string + default = "" + description = "Vault name for Terraform-native provisioning. Defaults to coder-." +} + +variable "provision_agent_name" { + type = string + default = "" + description = "Agent name for Terraform-native provisioning. Defaults to coder--agent." +} + +variable "provision_policy_path" { + type = string + default = "**" + description = "Secret path pattern for the auto-created access policy (Terraform-native mode)." +} + +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 +} From 577f567625f557420e8bde396f1a37a3628e3d07 Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:10:07 -0500 Subject: [PATCH 2/2] chore(oneclaw): add registry icon and point README at 1claw.svg Made-with: Cursor --- .icons/1claw.svg | 9 +++++++++ registry/kmjones1979/modules/oneclaw/README.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .icons/1claw.svg diff --git a/.icons/1claw.svg b/.icons/1claw.svg new file mode 100644 index 000000000..f6854deae --- /dev/null +++ b/.icons/1claw.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/registry/kmjones1979/modules/oneclaw/README.md b/registry/kmjones1979/modules/oneclaw/README.md index c0e3cde2e..171dff65b 100644 --- a/registry/kmjones1979/modules/oneclaw/README.md +++ b/registry/kmjones1979/modules/oneclaw/README.md @@ -1,7 +1,7 @@ --- display_name: 1Claw description: Vault-backed secrets and MCP server wiring for 1Claw in Coder workspaces -icon: ../../../../.icons/vault.svg +icon: ../../../../.icons/1claw.svg verified: false tags: [secrets, mcp, ai] ---