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/.images/avatar.png b/registry/kmjones1979/.images/avatar.png new file mode 100644 index 000000000..dd7f47e62 Binary files /dev/null and b/registry/kmjones1979/.images/avatar.png differ 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..559040181 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/README.md @@ -0,0 +1,74 @@ +--- +display_name: 1Claw +description: Vault-backed secrets and MCP server wiring for 1Claw in Coder workspaces +icon: ../../../../.icons/1claw.svg +verified: false +tags: [secrets, mcp, ai] +--- + +# 1Claw + +Give every Coder workspace scoped access to [1Claw](https://1claw.xyz) so AI coding agents can read secrets from an encrypted vault instead of hardcoded credentials. The module merges a `streamable-http` MCP server entry into Cursor and Claude Code config files without overwriting other MCP servers. + +Upstream source: [github.com/1clawAI/1claw-coder-workspace-module](https://github.com/1clawAI/1claw-coder-workspace-module). + +## Usage + +### Bootstrap mode (recommended) + +Creates a vault, agent, and access policy on the first workspace boot using a human `1ck_` API key, then caches credentials in `~/.1claw/bootstrap.json` for subsequent starts. + +```tf +module "oneclaw" { + source = "registry.coder.com/kmjones1979/oneclaw/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + human_api_key = var.oneclaw_human_key +} +``` + +#### Post-bootstrap cleanup (recommended) + +The `1ck_` human key is a privileged credential that can create and destroy vaults in your 1Claw account. It is only needed the first time the workspace boots. After the initial bootstrap succeeds: + +1. Clear the variable in your Terraform: + + ```tf + module "oneclaw" { + source = "registry.coder.com/kmjones1979/oneclaw/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + human_api_key = "" # scrubbed after first bootstrap + } + ``` + +2. Re-apply the template. On the next workspace start, the script loads credentials from `~/.1claw/bootstrap.json` and no longer references the human key. The workspace continues to work with the scoped `ocv_` agent key only. + +### Manual mode + +Pre-provision the vault and agent out-of-band and pass only the scoped `ocv_` agent key. Recommended for production and for threat models that include untrusted code running inside the workspace. + +```tf +module "oneclaw" { + source = "registry.coder.com/kmjones1979/oneclaw/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + vault_id = var.oneclaw_vault_id + api_token = var.oneclaw_agent_key +} +``` + +## Security notes + +The module is written so that the `1ck_` human bootstrap key leaves no persistent trace in the workspace: + +- The `ocv_` agent key exposed to the AI is scoped to a single vault and a single secret-path policy. That defines the blast radius of anything the AI does. +- The `1ck_` human key is injected into the bootstrap script as a sensitive `coder_env` variable (`_ONECLAW_HUMAN_API_KEY`), **never** templated into the script body. Because of this, it does **not** appear in `/tmp/coder-agent.log` (which records the rendered script) or in the Terraform state file's `coder_script` resource. The rendered script only contains the literal reference `HUMAN_KEY="${_ONECLAW_HUMAN_API_KEY:-}"`. +- During bootstrap, the human key is sent to the 1Claw API via `curl --data-binary @-` from stdin, so it never appears in process argv (`ps aux` / `/proc//cmdline`). +- The key is scrubbed from shell variables (`unset HUMAN_KEY` / `unset _ONECLAW_HUMAN_API_KEY`) immediately after authentication, so downstream processes started by the script do not inherit it. +- The key is **never** written to `~/.1claw/bootstrap.json`, `~/.cursor/mcp.json`, `~/.config/claude/mcp.json`, or any other on-disk file. Only the scoped `ocv_` agent key and the vault id are persisted. +- For highest assurance, use manual mode with a pre-provisioned `ocv_` key so the `1ck_` key never reaches the workspace at all. + +## Requirements + +Bootstrap mode runs inside the workspace and requires `curl` and `python3` in the container image. diff --git a/registry/kmjones1979/modules/oneclaw/main.test.ts b/registry/kmjones1979/modules/oneclaw/main.test.ts new file mode 100644 index 000000000..418b21de6 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, + findResourceInstance, +} from "~test"; + +describe("oneclaw", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent", + }); + + it("manual mode sets env vars and run script", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + api_token: "ocv_testtoken", + }); + + const vaultEnv = findResourceInstance(state, "coder_env", "vault_id"); + expect(vaultEnv.name).toBe("ONECLAW_VAULT_ID"); + + const apiKeyEnv = findResourceInstance(state, "coder_env", "agent_api_key"); + expect(apiKeyEnv.name).toBe("ONECLAW_AGENT_API_KEY"); + + const baseUrlEnv = findResourceInstance(state, "coder_env", "base_url"); + expect(baseUrlEnv.name).toBe("ONECLAW_BASE_URL"); + expect(baseUrlEnv.value).toBe("https://api.1claw.xyz"); + + const runScript = findResourceInstance(state, "coder_script", "run"); + expect(runScript.display_name).toBe("1Claw"); + expect(runScript.start_blocks_login).toBe(false); + + const provisions = state.resources.filter( + (r) => r.type === "null_resource" && r.name === "provision", + ); + expect(provisions.length).toBe(0); + }); + + it("bootstrap mode enables blocking run script and injects human key via coder_env", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + human_api_key: "1ck_test_human_key", + }); + + const runScript = findResourceInstance(state, "coder_script", "run"); + expect(runScript.display_name).toBe("1Claw"); + expect(runScript.start_blocks_login).toBe(true); + + // The human key is delivered via coder_env (sensitive), NOT baked into the + // script body, so it never lands in the Coder agent's script log. + const humanKeyEnv = findResourceInstance( + state, + "coder_env", + "human_api_key", + ); + expect(humanKeyEnv.name).toBe("_ONECLAW_HUMAN_API_KEY"); + + // And the actual key value must not appear anywhere in the rendered script text. + expect(runScript.script).not.toContain("1ck_test_human_key"); + // The script must reference the env var, not a literal value. + expect(runScript.script).toContain("_ONECLAW_HUMAN_API_KEY"); + + const provisions = state.resources.filter( + (r) => r.type === "null_resource" && r.name === "provision", + ); + expect(provisions.length).toBe(0); + }); + + it("custom base_url is reflected in env", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "test-agent", + vault_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + api_token: "ocv_testtoken", + base_url: "https://api.example.com", + }); + + const baseUrlEnv = findResourceInstance(state, "coder_env", "base_url"); + expect(baseUrlEnv.value).toBe("https://api.example.com"); + }); +}); diff --git a/registry/kmjones1979/modules/oneclaw/main.tf b/registry/kmjones1979/modules/oneclaw/main.tf new file mode 100644 index 000000000..b85bb2e1f --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.tf @@ -0,0 +1,223 @@ +terraform { + required_version = ">= 1.4" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.12" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "vault_id" { + type = string + description = "The 1Claw vault ID to scope MCP access to. Optional when using bootstrap mode (human_api_key)." + default = "" + + validation { + condition = var.vault_id == "" || can(regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", var.vault_id)) + error_message = "vault_id must be a valid UUID or empty (for bootstrap mode)." + } +} + +variable "api_token" { + type = string + sensitive = true + description = "1Claw agent API key (starts with ocv_). Optional when using bootstrap mode (human_api_key)." + default = "" +} + +variable "human_api_key" { + type = string + sensitive = true + default = "" + description = "One-time human 1ck_ API key for auto-provisioning. On first workspace start, creates a vault, agent, and policy automatically. Credentials are cached in ~/.1claw/bootstrap.json for subsequent starts." +} + +variable "bootstrap_vault_name" { + type = string + default = "coder-workspace" + description = "Name for the auto-created vault (only used when vault_id is not provided and human_api_key is set)." +} + +variable "bootstrap_agent_name" { + type = string + default = "" + description = "Name for the auto-created agent. Defaults to coder-." +} + +variable "bootstrap_policy_path" { + type = string + default = "**" + description = "Secret path pattern for the auto-created policy (glob). Defaults to all secrets." +} + +variable "agent_id_1claw" { + type = string + description = "Optional 1Claw agent UUID. When omitted, the MCP server resolves the agent from the API key prefix." + default = "" +} + +variable "mcp_host" { + type = string + description = "Base URL of the 1Claw MCP server." + default = "https://mcp.1claw.xyz/mcp" + + validation { + condition = can(regex("^https?://", var.mcp_host)) + error_message = "mcp_host must start with http:// or https://." + } +} + +variable "base_url" { + type = string + description = "Base URL of the 1Claw Vault API (used by ONECLAW_BASE_URL env var)." + default = "https://api.1claw.xyz" + + validation { + condition = can(regex("^https?://", var.base_url)) + error_message = "base_url must start with http:// or https://." + } +} + +variable "install_cursor_config" { + type = bool + description = "Whether to write MCP config to the Cursor IDE config path." + default = true +} + +variable "install_claude_config" { + type = bool + description = "Whether to write MCP config to the Claude Code config path." + default = true +} + +variable "cursor_config_path" { + type = string + description = "Path where the Cursor MCP config file is written." + default = "$HOME/.cursor/mcp.json" +} + +variable "claude_config_path" { + type = string + description = "Path where the Claude Code MCP config file is written." + default = "$HOME/.config/claude/mcp.json" +} + +variable "icon" { + type = string + description = "Icon to display for the setup script in the Coder UI." + default = "/icon/vault.svg" +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation." + default = null +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +locals { + bootstrap_mode = var.human_api_key != "" + bootstrap_agent_name = ( + var.bootstrap_agent_name != "" ? var.bootstrap_agent_name : + "coder-${data.coder_workspace.me.name}" + ) +} + +resource "coder_env" "vault_id" { + count = var.vault_id != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_VAULT_ID" + value = var.vault_id +} + +resource "coder_env" "agent_api_key" { + count = var.api_token != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_AGENT_API_KEY" + value = var.api_token +} + +resource "coder_env" "oneclaw_agent_id" { + count = var.agent_id_1claw != "" ? 1 : 0 + agent_id = var.agent_id + name = "ONECLAW_AGENT_ID" + value = var.agent_id_1claw +} + +resource "coder_env" "base_url" { + agent_id = var.agent_id + name = "ONECLAW_BASE_URL" + value = var.base_url +} + +# Sensitive values are passed via coder_env (not templated into the script body) +# so they don't appear in the Coder agent's script log. The agent log is 0600 on +# the coder user, but that's the same user the AI runs as in most images, so we +# want to avoid any on-disk copy of the 1ck_ key in the workspace. +resource "coder_env" "human_api_key" { + count = local.bootstrap_mode ? 1 : 0 + agent_id = var.agent_id + name = "_ONECLAW_HUMAN_API_KEY" + value = var.human_api_key +} + +resource "coder_script" "run" { + agent_id = var.agent_id + display_name = "1Claw" + icon = var.icon + run_on_start = true + start_blocks_login = local.bootstrap_mode + + script = templatefile("${path.module}/scripts/run.sh", { + BOOTSTRAP_MODE = local.bootstrap_mode ? "true" : "false" + BASE_URL = var.base_url + VAULT_ID_INPUT = var.vault_id + VAULT_NAME = var.bootstrap_vault_name + AGENT_NAME = local.bootstrap_agent_name + POLICY_PATH = var.bootstrap_policy_path + STATE_DIR = "$HOME/.1claw" + MCP_HOST = var.mcp_host + INSTALL_CURSOR_CONFIG = var.install_cursor_config ? "true" : "false" + INSTALL_CLAUDE_CONFIG = var.install_claude_config ? "true" : "false" + CURSOR_CONFIG_PATH = var.cursor_config_path + CLAUDE_CONFIG_PATH = var.claude_config_path + }) +} + +output "mcp_config_path" { + description = "Primary MCP config file path (Cursor). Use this to reference the config from downstream resources." + value = var.cursor_config_path +} + +output "claude_config_path" { + description = "Claude Code MCP config file path." + value = var.install_claude_config ? var.claude_config_path : "" +} + +output "vault_id" { + description = "The 1Claw vault ID configured for this workspace (manual mode only; bootstrap mode resolves the vault ID inside the workspace)." + value = var.vault_id + sensitive = true +} + +output "agent_id_1claw" { + description = "The 1Claw agent UUID, if provided via variable." + value = var.agent_id_1claw + sensitive = true +} + +output "provisioning_mode" { + description = "Which provisioning mode is active: bootstrap or manual." + value = local.bootstrap_mode ? "bootstrap" : "manual" + sensitive = true +} diff --git a/registry/kmjones1979/modules/oneclaw/main.tftest.hcl b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl new file mode 100644 index 000000000..e792e0d30 --- /dev/null +++ b/registry/kmjones1979/modules/oneclaw/main.tftest.hcl @@ -0,0 +1,99 @@ +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.vault_id) == 1 + error_message = "ONECLAW_VAULT_ID should be set in manual mode" + } + + assert { + condition = length(coder_env.agent_api_key) == 1 + error_message = "ONECLAW_AGENT_API_KEY should be set in manual mode" + } + + assert { + condition = coder_script.run.start_blocks_login == false + error_message = "Manual mode should not block login" + } + + assert { + condition = output.provisioning_mode == "manual" + error_message = "provisioning_mode should be 'manual' when no human_api_key is set" + } +} + +run "bootstrap_mode" { + command = plan + + variables { + agent_id = "test-agent-bootstrap" + human_api_key = "1ck_test_human_key" + } + + assert { + condition = coder_script.run.start_blocks_login == true + error_message = "Bootstrap mode should block login while provisioning" + } + + assert { + condition = length(coder_env.vault_id) == 0 + error_message = "No vault_id env var in pure bootstrap mode (resolved inside workspace)" + } + + assert { + condition = length(coder_env.agent_api_key) == 0 + error_message = "No agent_api_key env var in pure bootstrap mode (resolved inside workspace)" + } + + assert { + condition = length(coder_env.human_api_key) == 1 + error_message = "Bootstrap mode should inject _ONECLAW_HUMAN_API_KEY via coder_env" + } + + assert { + 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 = output.provisioning_mode == "bootstrap" + error_message = "provisioning_mode should be 'bootstrap' when human_api_key is set" + } +} + +run "manual_mode_no_human_key_env" { + command = plan + + variables { + agent_id = "test-agent-manual-noenv" + vault_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + api_token = "ocv_testtoken" + } + + assert { + condition = length(coder_env.human_api_key) == 0 + error_message = "Manual mode should not inject _ONECLAW_HUMAN_API_KEY" + } +} + +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.base_url.value == "https://api.example.com" + error_message = "ONECLAW_BASE_URL should match base_url" + } +} 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"