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/3] 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/3] 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] --- From 7ba20935d53433b8375d7e9e0eef610c545d8e69 Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Tue, 21 Apr 2026 21:06:52 -0700 Subject: [PATCH 3/3] refactor(oneclaw): consolidate structure and harden bootstrap key handling Addresses reviewer feedback on closed PR #845 that the module was "split up way more than usual" and did not follow the registry module schema. Structure (matches the coder/ namespace conventions): - Collapse variables.tf + outputs.tf into main.tf - Merge scripts/bootstrap.sh + scripts/setup.sh into a single scripts/run.sh executed by a single coder_script - Remove Terraform-native provisioning mode (scripts/provision.sh, null_resource.provision, master_api_key): it relied on local-exec writing a state file to the provisioner's cwd, which is ephemeral inside Coder template provisioners and therefore cannot round-trip credentials into coder_env - Keep two supported modes: bootstrap (human 1ck_ key, recommended) and manual (pre-provisioned scoped ocv_ key) Security hardening for the 1ck_ human bootstrap key: - Deliver the key via a sensitive coder_env (_ONECLAW_HUMAN_API_KEY) instead of templatefile() substitution, so the literal key never appears in the rendered script body stored in Terraform state or logged to the workspace's /tmp/coder-agent.log - Send the key to the 1Claw auth endpoint via curl --data-binary @- from stdin so it does not appear in process argv (ps/proc/cmdline) - Unset HUMAN_KEY and _ONECLAW_HUMAN_API_KEY as soon as auth completes so downstream processes do not inherit the key - Only the scoped ocv_ agent key and vault id are persisted to ~/.1claw/bootstrap.json and the MCP config files - README documents post-bootstrap cleanup (set human_api_key = "" once the state file exists) and the full security guarantees Tested end-to-end against a local Coder server with real 1Claw credentials: first boot, idempotent restart, and post-bootstrap cleanup all succeed and leave no copy of the 1ck_ value anywhere on the workspace filesystem or in its process environments. Made-with: Cursor --- .../kmjones1979/modules/oneclaw/README.md | 57 +-- .../kmjones1979/modules/oneclaw/main.test.ts | 65 ++-- registry/kmjones1979/modules/oneclaw/main.tf | 327 +++++++++--------- .../modules/oneclaw/main.tftest.hcl | 66 ++-- .../kmjones1979/modules/oneclaw/outputs.tf | 33 -- .../modules/oneclaw/scripts/bootstrap.sh | 151 -------- .../modules/oneclaw/scripts/provision.sh | 151 -------- .../modules/oneclaw/scripts/run.sh | 246 +++++++++++++ .../modules/oneclaw/scripts/setup.sh | 124 ------- .../kmjones1979/modules/oneclaw/variables.tf | 153 -------- 10 files changed, 505 insertions(+), 868 deletions(-) delete mode 100644 registry/kmjones1979/modules/oneclaw/outputs.tf delete mode 100644 registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh delete mode 100755 registry/kmjones1979/modules/oneclaw/scripts/provision.sh create mode 100755 registry/kmjones1979/modules/oneclaw/scripts/run.sh delete mode 100644 registry/kmjones1979/modules/oneclaw/scripts/setup.sh delete mode 100644 registry/kmjones1979/modules/oneclaw/variables.tf diff --git a/registry/kmjones1979/modules/oneclaw/README.md b/registry/kmjones1979/modules/oneclaw/README.md index 171dff65b..559040181 100644 --- a/registry/kmjones1979/modules/oneclaw/README.md +++ b/registry/kmjones1979/modules/oneclaw/README.md @@ -8,28 +8,45 @@ 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. +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 -### Terraform-native mode (recommended) +### Bootstrap mode (recommended) -Provisions vault, agent, and access policy at `terraform apply`; cleans up on `terraform destroy`. +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 - master_api_key = var.oneclaw_key + 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 -Use an existing vault and agent API key from the 1Claw dashboard. +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" { @@ -41,21 +58,17 @@ module "oneclaw" { } ``` -### Shell bootstrap mode +## Security notes -Creates vault and agent on the first workspace boot, then caches credentials for subsequent starts. +The module is written so that the `1ck_` human bootstrap key leaves no persistent trace in the workspace: -```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 -} -``` +- 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//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. -> [!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`. +## Requirements -> [!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. +Bootstrap mode runs inside the workspace and requires `curl` and `python3` in the container image. diff --git a/registry/kmjones1979/modules/oneclaw/main.test.ts b/registry/kmjones1979/modules/oneclaw/main.test.ts index 89e03d8e8..418b21de6 100644 --- a/registry/kmjones1979/modules/oneclaw/main.test.ts +++ b/registry/kmjones1979/modules/oneclaw/main.test.ts @@ -13,68 +13,59 @@ describe("oneclaw", async () => { agent_id: "test-agent", }); - it("manual mode sets env vars and mcp script", async () => { + 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", - "oneclaw_vault_id", - ); + const vaultEnv = findResourceInstance(state, "coder_env", "vault_id"); expect(vaultEnv.name).toBe("ONECLAW_VAULT_ID"); - const apiKeyEnv = findResourceInstance( - state, - "coder_env", - "oneclaw_agent_api_key", - ); + const apiKeyEnv = findResourceInstance(state, "coder_env", "agent_api_key"); expect(apiKeyEnv.name).toBe("ONECLAW_AGENT_API_KEY"); - const baseUrlEnv = findResourceInstance( - state, - "coder_env", - "oneclaw_base_url", - ); + const baseUrlEnv = findResourceInstance(state, "coder_env", "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 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 === "oneclaw_provision", + (r) => r.type === "null_resource" && r.name === "provision", ); expect(provisions.length).toBe(0); }); - it("bootstrap mode creates bootstrap script", async () => { + 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 bootstrap = findResourceInstance( + 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_script", - "oneclaw_bootstrap", + "coder_env", + "human_api_key", ); - expect(bootstrap.display_name).toBe("1Claw Bootstrap"); + 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 === "oneclaw_provision", + (r) => r.type === "null_resource" && r.name === "provision", ); expect(provisions.length).toBe(0); }); @@ -87,11 +78,7 @@ describe("oneclaw", async () => { base_url: "https://api.example.com", }); - const baseUrlEnv = findResourceInstance( - state, - "coder_env", - "oneclaw_base_url", - ); + const baseUrlEnv = findResourceInstance(state, "coder_env", "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 index 3dbabfa98..b85bb2e1f 100644 --- a/registry/kmjones1979/modules/oneclaw/main.tf +++ b/registry/kmjones1979/modules/oneclaw/main.tf @@ -6,211 +6,218 @@ terraform { 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 +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} - provision_state_file = "${path.module}/.provision-state.json" +variable "vault_id" { + type = string + description = "The 1Claw vault ID to scope MCP access to. Optional when using bootstrap mode (human_api_key)." + default = "" - 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" - ) + 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)." + } +} - # 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 +variable "api_token" { + type = string + sensitive = true + description = "1Claw agent API key (starts with ocv_). Optional when using bootstrap mode (human_api_key)." + default = "" +} - # 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)) : {} +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." +} - 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", "") +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)." } -data "coder_workspace" "me" {} +variable "bootstrap_agent_name" { + type = string + default = "" + description = "Name for the auto-created agent. Defaults to coder-." +} -data "coder_workspace_owner" "me" {} +variable "bootstrap_policy_path" { + type = string + default = "**" + description = "Secret path pattern for the auto-created policy (glob). Defaults to all secrets." +} -# =========================================================================== -# 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) - } +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" - 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 - }) + 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" - 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 + validation { + condition = can(regex("^https?://", var.base_url)) + error_message = "base_url must start with http:// or https://." } } -# =========================================================================== -# Environment variables (injected into the workspace agent) -# =========================================================================== +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" {} -resource "coder_env" "oneclaw_vault_id" { - count = local.effective_vault_id != "" ? 1 : 0 +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 = local.effective_vault_id + value = var.vault_id } -resource "coder_env" "oneclaw_agent_api_key" { - count = local.effective_token != "" ? 1 : 0 +resource "coder_env" "agent_api_key" { + count = var.api_token != "" ? 1 : 0 agent_id = var.agent_id name = "ONECLAW_AGENT_API_KEY" - value = local.effective_token + value = var.api_token } resource "coder_env" "oneclaw_agent_id" { - count = var.agent_id_1claw != "" || local.provisioned_agent_id != "" ? 1 : 0 + count = var.agent_id_1claw != "" ? 1 : 0 agent_id = var.agent_id name = "ONECLAW_AGENT_ID" - value = var.agent_id_1claw != "" ? var.agent_id_1claw : local.provisioned_agent_id + value = var.agent_id_1claw } -resource "coder_env" "oneclaw_base_url" { +resource "coder_env" "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" - }) +# 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 } -# =========================================================================== -# MCP config file injection -# =========================================================================== - -resource "coder_script" "oneclaw_mcp_setup" { +resource "coder_script" "run" { agent_id = var.agent_id - display_name = "1Claw MCP Setup" + display_name = "1Claw" icon = var.icon run_on_start = true - start_blocks_login = false + start_blocks_login = local.bootstrap_mode - script = templatefile("${path.module}/scripts/setup.sh", { - MCP_HOST = var.mcp_host - VAULT_ID = local.effective_vault_id - API_TOKEN = local.effective_token + script = templatefile("${path.module}/scripts/run.sh", { BOOTSTRAP_MODE = local.bootstrap_mode ? "true" : "false" - INSTALL_CURSOR_CONFIG = var.install_cursor_config - INSTALL_CLAUDE_CONFIG = var.install_claude_config + 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 +} diff --git a/registry/kmjones1979/modules/oneclaw/main.tftest.hcl b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl index 9c8ee927a..e792e0d30 100644 --- a/registry/kmjones1979/modules/oneclaw/main.tftest.hcl +++ b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl @@ -8,81 +8,77 @@ run "manual_mode" { } assert { - condition = length(coder_env.oneclaw_vault_id) == 1 + condition = length(coder_env.vault_id) == 1 error_message = "ONECLAW_VAULT_ID should be set in manual mode" } assert { - condition = length(coder_env.oneclaw_agent_api_key) == 1 + condition = length(coder_env.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" + condition = coder_script.run.start_blocks_login == false + error_message = "Manual mode should not block login" } assert { - condition = length(coder_script.oneclaw_bootstrap) == 0 - error_message = "No bootstrap script in manual mode" + condition = output.provisioning_mode == "manual" + error_message = "provisioning_mode should be 'manual' when no human_api_key is set" } } -run "terraform_native_mode" { +run "bootstrap_mode" { command = plan variables { - agent_id = "test-agent-tf" - master_api_key = "1ck_test_master_key" + agent_id = "test-agent-bootstrap" + human_api_key = "1ck_test_human_key" } assert { - condition = length(null_resource.oneclaw_provision) == 1 - error_message = "Terraform-native mode should create the provision null_resource" + condition = coder_script.run.start_blocks_login == true + error_message = "Bootstrap mode should block login while provisioning" } assert { - condition = length(coder_script.oneclaw_bootstrap) == 0 - error_message = "No bootstrap script in terraform-native mode" + condition = length(coder_env.vault_id) == 0 + error_message = "No vault_id env var in pure bootstrap mode (resolved inside workspace)" } -} -run "bootstrap_mode" { - command = plan + assert { + condition = length(coder_env.agent_api_key) == 0 + error_message = "No agent_api_key env var in pure bootstrap mode (resolved inside workspace)" + } - variables { - agent_id = "test-agent-bootstrap" - human_api_key = "1ck_test_human_key" + assert { + condition = length(coder_env.human_api_key) == 1 + error_message = "Bootstrap mode should inject _ONECLAW_HUMAN_API_KEY via coder_env" } assert { - condition = length(coder_script.oneclaw_bootstrap) == 1 - error_message = "Bootstrap mode should create the bootstrap script" + condition = coder_env.human_api_key[0].name == "_ONECLAW_HUMAN_API_KEY" + error_message = "Human key env var should be named _ONECLAW_HUMAN_API_KEY" } assert { - condition = length(null_resource.oneclaw_provision) == 0 - error_message = "No provision resource in bootstrap mode" + condition = output.provisioning_mode == "bootstrap" + error_message = "provisioning_mode should be 'bootstrap' when human_api_key is set" } } -run "master_key_takes_precedence_over_human" { +run "manual_mode_no_human_key_env" { 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" + agent_id = "test-agent-manual-noenv" + vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + api_token = "ocv_testtoken" } assert { - condition = length(coder_script.oneclaw_bootstrap) == 0 - error_message = "No bootstrap script when master_api_key is set" + condition = length(coder_env.human_api_key) == 0 + error_message = "Manual mode should not inject _ONECLAW_HUMAN_API_KEY" } } @@ -97,7 +93,7 @@ run "custom_base_url" { } assert { - condition = coder_env.oneclaw_base_url.value == "https://api.example.com" + condition = coder_env.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 deleted file mode 100644 index f106b092a..000000000 --- a/registry/kmjones1979/modules/oneclaw/outputs.tf +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 0faeeabaa..000000000 --- a/registry/kmjones1979/modules/oneclaw/scripts/bootstrap.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/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 deleted file mode 100755 index 893b7afff..000000000 --- a/registry/kmjones1979/modules/oneclaw/scripts/provision.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/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/run.sh b/registry/kmjones1979/modules/oneclaw/scripts/run.sh new file mode 100755 index 000000000..1942c9456 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/scripts/run.sh @@ -0,0 +1,246 @@ +#!/bin/bash +set -euo pipefail + +LOG_PREFIX="[1claw]" +log() { echo "$LOG_PREFIX $*"; } +die() { + log "ERROR: $*" >&2 + exit 1 +} + +BOOTSTRAP_MODE="${BOOTSTRAP_MODE}" +API_URL="${BASE_URL}" +VAULT_ID_INPUT="${VAULT_ID_INPUT}" +VAULT_NAME_IN="${VAULT_NAME}" +AGENT_NAME_IN="${AGENT_NAME}" +POLICY_PATH_IN="${POLICY_PATH}" +STATE_DIR=$(eval echo "${STATE_DIR}") +STATE_FILE="$STATE_DIR/bootstrap.json" + +# Sensitive values come from env vars injected by coder_env (sensitive = true), +# NOT from templatefile() substitutions, so they do not appear in the Coder +# agent's rendered-script log (/tmp/coder-agent.log). +HUMAN_KEY="$${_ONECLAW_HUMAN_API_KEY:-}" +API_TOKEN="$${ONECLAW_AGENT_API_KEY:-}" +VAULT_ID="$${ONECLAW_VAULT_ID:-}" + +json_get() { + python3 -c "import json,sys; print(json.load(sys.stdin)$1)" +} + +api_call() { + local method="$1" path="$2" token="$3" body="$${4:-}" + local response http_code body_out + # Pass bearer token via stdin config to keep it out of process argv. + # Body (if any) is piped on stdin as --data-binary. + local curl_cfg + curl_cfg=$(mktemp) + printf -- 'header = "Authorization: Bearer %s"\n' "$token" > "$curl_cfg" + if [ -n "$body" ]; then + response=$(printf '%s' "$body" | curl -s -w "\n%%{http_code}" \ + -K "$curl_cfg" \ + -H "Content-Type: application/json" \ + --data-binary @- \ + -X "$method" "$API_URL$path" 2>&1) + else + response=$(curl -s -w "\n%%{http_code}" \ + -K "$curl_cfg" \ + -H "Content-Type: application/json" \ + -X "$method" "$API_URL$path" 2>&1) + fi + local rc=$? + rm -f "$curl_cfg" + if [ $rc -ne 0 ]; then + log "API call failed: $method $path" + return 1 + fi + http_code=$(echo "$response" | tail -1) + 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" +} + +bootstrap() { + if [ -f "$STATE_FILE" ]; then + log "Bootstrap state found at $STATE_FILE — skipping provisioning" + return 0 + fi + + [ -n "$HUMAN_KEY" ] || die "human_api_key is required for bootstrap mode" + + log "Authenticating with 1Claw API..." + local auth_response auth_http auth_body jwt + # Pipe the body via stdin so the 1ck_ key never appears in process argv (ps/proc/cmdline). + auth_response=$(printf '{"api_key": "%s"}' "$HUMAN_KEY" | curl -s -w "\n%%{http_code}" \ + -H "Content-Type: application/json" \ + --data-binary @- \ + "$API_URL/v1/auth/api-key-token" 2>&1) || die "Failed to authenticate with human API key" + + # Key is no longer needed; scrub from process memory before any other work. + HUMAN_KEY="" + unset HUMAN_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)" + fi + jwt=$(echo "$auth_body" | json_get "['access_token']") + auth_body="" + auth_response="" + log "Authenticated successfully" + + local vault="$VAULT_ID_INPUT" + if [ -n "$vault" ]; then + log "Using provided vault: $vault" + else + log "Creating vault '$VAULT_NAME_IN'..." + local vault_response + vault_response=$(api_call POST "/v1/vaults" "$jwt" \ + "{\"name\": \"$VAULT_NAME_IN\"}") || { + log "Vault creation failed — looking for existing vault named '$VAULT_NAME_IN'" + local list_response + list_response=$(api_call GET "/v1/vaults" "$jwt") || die "Failed to list vaults" + vault=$(echo "$list_response" | python3 -c " +import json, sys +for v in json.load(sys.stdin).get('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 + + log "Creating agent '$AGENT_NAME_IN'..." + local agent_response agent_id agent_key + 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_key=$(echo "$agent_response" | json_get "['api_key']") + if [ -z "$agent_key" ] || [ "$agent_key" = "None" ]; then + die "Agent created but no API key returned" + fi + log "Created agent: $agent_id" + + 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" + + mkdir -p "$STATE_DIR" + python3 - "$STATE_FILE" "$vault" "$agent_id" "$agent_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" + + jwt="" + unset jwt + + log "Bootstrap complete — credentials saved to $STATE_FILE" + log " Vault: $vault" + log " Agent: $agent_id" +} + +write_mcp_config() { + local target_path="$1" label="$2" tmp_file="$3" + target_path=$(eval echo "$target_path") + local target_dir + target_dir=$(dirname "$target_path") + [ -d "$target_dir" ] || mkdir -p "$target_dir" + + if [ -f "$target_path" ]; then + log "Merging 1Claw MCP server into existing $label config at $target_path" + python3 - "$target_path" "$tmp_file" << 'PYEOF' +import json, sys +target_path = sys.argv[1] +new_config_path = sys.argv[2] +try: + with open(target_path) as f: + existing = json.load(f) +except (json.JSONDecodeError, FileNotFoundError): + existing = {} +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 "Writing $label MCP config to $target_path" + cp "$tmp_file" "$target_path" + fi + + chmod 600 "$target_path" + log "$label MCP config ready at $target_path" +} + +if [ "$BOOTSTRAP_MODE" = "true" ]; then + bootstrap +fi + +# Scrub the human bootstrap key from both the local var and the inherited env, +# so downstream processes (shells, AI agents) cannot read it from this script's +# /proc//environ or from their own inherited environment. +HUMAN_KEY="" +unset HUMAN_KEY +unset _ONECLAW_HUMAN_API_KEY + +# Bootstrap runs first and writes creds to the state file; load them now. +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 set human_api_key/master_api_key" + exit 0 +fi + +MCP_CONFIG_TMP=$(mktemp) +trap 'rm -f "$MCP_CONFIG_TMP"' EXIT + +python3 - "$API_TOKEN" "$VAULT_ID" "${MCP_HOST}" > "$MCP_CONFIG_TMP" << 'PYEOF' +import json, sys +config = { + "mcpServers": { + "1claw": { + "url": sys.argv[3], + "headers": { + "Authorization": "Bearer " + sys.argv[1], + "X-Vault-ID": sys.argv[2] + } + } + } +} +print(json.dumps(config, indent=2)) +PYEOF + +if [ "${INSTALL_CURSOR_CONFIG}" = "true" ]; then + write_mcp_config "${CURSOR_CONFIG_PATH}" "Cursor" "$MCP_CONFIG_TMP" +fi + +if [ "${INSTALL_CLAUDE_CONFIG}" = "true" ]; then + write_mcp_config "${CLAUDE_CONFIG_PATH}" "Claude Code" "$MCP_CONFIG_TMP" +fi + +log "1Claw setup complete" diff --git a/registry/kmjones1979/modules/oneclaw/scripts/setup.sh b/registry/kmjones1979/modules/oneclaw/scripts/setup.sh deleted file mode 100644 index 3286531c8..000000000 --- a/registry/kmjones1979/modules/oneclaw/scripts/setup.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/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 deleted file mode 100644 index 564b902d7..000000000 --- a/registry/kmjones1979/modules/oneclaw/variables.tf +++ /dev/null @@ -1,153 +0,0 @@ -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 -}