From ea8141cdfa193569db2f26b528506608e7557b33 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/5] 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 9eb71ec79420a43d41c56cc9b4db1e2d10fa6734 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/5] 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 868cb2a1d9625b4b88a944a6ccb925069d32c8c0 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/5] 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 -} From 86bfca8655f3352cc7ae4f4987c21f4b639cf48a Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Sun, 21 Jun 2026 07:08:29 -0500 Subject: [PATCH 4/5] fix(oneclaw): address Copilot review feedback on PR #857 main.tf: - Remove unused `order` variable. coder_script does not support an order field, so the variable was dead code. - Remove unused `data "coder_workspace_owner" "me"` source. scripts/run.sh: - Replace `eval echo` (used to expand `$HOME` / `~` in user-overridable config paths) with a dedicated expand_path() helper that uses bash string substitution and a case statement. Adds unit-test coverage confirming that injected `$(...)` and backticks are not executed. - Rewrite the existing-vault lookup so the vault name is passed to Python via argv instead of being interpolated into the inline Python source. The JSON payload is now fed to Python on stdin via a here-string instead of via a pipe + here-doc combination, which shellcheck flagged (SC2259) as broken (the heredoc would have overridden the piped stdin and the program would have failed to parse the response). - `unset _ONECLAW_HUMAN_API_KEY` immediately after reading it into HUMAN_KEY at the top of the script, so the privileged bootstrap key is no longer visible in /proc//environ for the lifetime of the script run. - Update the "no API token or vault ID" warning to drop the mention of `master_api_key`, which was removed earlier in this PR. Made-with: Cursor --- registry/kmjones1979/modules/oneclaw/main.tf | 8 --- .../modules/oneclaw/scripts/run.sh | 49 ++++++++++++++----- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/registry/kmjones1979/modules/oneclaw/main.tf b/registry/kmjones1979/modules/oneclaw/main.tf index b85bb2e1f..8ff7bb40a 100644 --- a/registry/kmjones1979/modules/oneclaw/main.tf +++ b/registry/kmjones1979/modules/oneclaw/main.tf @@ -115,16 +115,8 @@ variable "icon" { default = "/icon/vault.svg" } -variable "order" { - type = number - description = "The order determines the position of app in the UI presentation." - default = null -} - data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} - locals { bootstrap_mode = var.human_api_key != "" bootstrap_agent_name = ( diff --git a/registry/kmjones1979/modules/oneclaw/scripts/run.sh b/registry/kmjones1979/modules/oneclaw/scripts/run.sh index 1942c9456..9dc4c6542 100755 --- a/registry/kmjones1979/modules/oneclaw/scripts/run.sh +++ b/registry/kmjones1979/modules/oneclaw/scripts/run.sh @@ -8,19 +8,38 @@ die() { exit 1 } +# expand_path replaces a leading "~" and any literal "$HOME" occurrences with the +# workspace user's $HOME without invoking `eval`, which would allow arbitrary +# command substitution if a templated path ever contained $(...) or backticks. +expand_path() { + local p="$1" + p="$${p//\$HOME/$HOME}" + # The case patterns below match a literal "~" and "~/"; we deliberately + # do not want shell tilde expansion here, hence the literal quoting. + # shellcheck disable=SC2088 + case "$p" in + "~") p="$HOME" ;; + "~/"*) p="$HOME/$${p#\~/}" ;; + esac + printf '%s' "$p" +} + 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_DIR=$(expand_path "${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). +# agent's rendered-script log (/tmp/coder-agent.log). The bootstrap key is also +# unset from the env immediately after read so it does not linger in this +# process's /proc//environ for the duration of the run. HUMAN_KEY="$${_ONECLAW_HUMAN_API_KEY:-}" +unset _ONECLAW_HUMAN_API_KEY API_TOKEN="$${ONECLAW_AGENT_API_KEY:-}" VAULT_ID="$${ONECLAW_VAULT_ID:-}" @@ -81,6 +100,7 @@ bootstrap() { "$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. + # (_ONECLAW_HUMAN_API_KEY was already unset at the top of the script.) HUMAN_KEY="" unset HUMAN_KEY @@ -105,13 +125,17 @@ bootstrap() { 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 " + # Pass the vault name via argv (NOT interpolated into the Python source) + # so quotes/newlines in the name cannot break the program or be used for + # code injection. The JSON payload is fed on stdin via a here-string. + vault=$(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) +target = sys.argv[1] +for v in json.load(sys.stdin).get("vaults", []): + if v["name"] == target: + print(v["id"]); sys.exit(0) sys.exit(1) -") || die "Could not find existing vault named '$VAULT_NAME_IN'" +' "$VAULT_NAME_IN" <<< "$list_response") || die "Could not find existing vault named '$VAULT_NAME_IN'" log "Found existing vault: $vault" } if [ -z "$vault" ]; then @@ -161,7 +185,7 @@ PYEOF write_mcp_config() { local target_path="$1" label="$2" tmp_file="$3" - target_path=$(eval echo "$target_path") + target_path=$(expand_path "$target_path") local target_dir target_dir=$(dirname "$target_path") [ -d "$target_dir" ] || mkdir -p "$target_dir" @@ -196,12 +220,11 @@ 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. +# Belt-and-suspenders: ensure HUMAN_KEY is gone even if bootstrap() short- +# circuited because the state file already existed. _ONECLAW_HUMAN_API_KEY was +# already unset at the top of the script so it is no longer in our environ. 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 @@ -212,7 +235,7 @@ 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" + log "Provide api_token + vault_id (manual mode), or set human_api_key (bootstrap mode)" exit 0 fi From 69ea47981f12109baf50ce6c5b2c4008b9e94603 Mon Sep 17 00:00:00 2001 From: Kevin J <6829515+kmjones1979@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:53:23 -0500 Subject: [PATCH 5/5] fix(oneclaw): add canonical Terraform code block under README h1 The repo's README validator (cmd/readmevalidation) requires every module README to contain exactly one fenced ```tf code block (with a `version` field) between the H1 and the next heading. The previous structure had the H1 followed directly by `## Usage`, with the tf example nested inside `### Bootstrap mode`, so the validator failed with: "registry/kmjones1979/modules/oneclaw/README.md": did not find Terraform code block within h1 section Move the canonical bootstrap-mode example up to sit directly under the H1 paragraph (matching the convention used by registry/coder/modules/*), and drop the now-duplicate block from the Bootstrap mode subsection. Made-with: Cursor --- registry/kmjones1979/modules/oneclaw/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/kmjones1979/modules/oneclaw/README.md b/registry/kmjones1979/modules/oneclaw/README.md index 559040181..9b7c25534 100644 --- a/registry/kmjones1979/modules/oneclaw/README.md +++ b/registry/kmjones1979/modules/oneclaw/README.md @@ -10,14 +10,6 @@ tags: [secrets, mcp, ai] Give every Coder workspace scoped access to [1Claw](https://1claw.xyz) so AI coding agents can read secrets from an encrypted vault instead of hardcoded credentials. The module merges a `streamable-http` MCP server entry into Cursor and Claude Code config files without overwriting other MCP servers. -Upstream source: [github.com/1clawAI/1claw-coder-workspace-module](https://github.com/1clawAI/1claw-coder-workspace-module). - -## Usage - -### Bootstrap mode (recommended) - -Creates a vault, agent, and access policy on the first workspace boot using a human `1ck_` API key, then caches credentials in `~/.1claw/bootstrap.json` for subsequent starts. - ```tf module "oneclaw" { source = "registry.coder.com/kmjones1979/oneclaw/coder" @@ -27,6 +19,14 @@ module "oneclaw" { } ``` +Upstream source: [github.com/1clawAI/1claw-coder-workspace-module](https://github.com/1clawAI/1claw-coder-workspace-module). + +## Usage + +### Bootstrap mode (recommended) + +Creates a vault, agent, and access policy on the first workspace boot using a human `1ck_` API key, then caches credentials in `~/.1claw/bootstrap.json` for subsequent starts. This is the configuration shown at the top of this README. + #### 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: