Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Underlying providers are routed automatically and grow over time — your agent
<details>
<summary><b>Is it safe?</b></summary>

Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. The only data AgentKey collects is anonymous usage telemetry — which agent you installed into, your skill version, and upgrade outcomes — never your queries or responses. See "How do I opt out of telemetry?" below.
Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. The only data AgentKey collects is anonymous usage telemetry — which agent you installed into or removed from, your skill version, and upgrade outcomes — never your queries or responses. See "How do I opt out of telemetry?" below.

</details>

Expand Down Expand Up @@ -230,7 +230,7 @@ The one-command uninstaller additionally cleans npm/npx caches, legacy shell rc
<details>
<summary><b>How do I opt out of telemetry?</b></summary>

AgentKey sends anonymous usage telemetry (which agent you use, skill version, upgrade outcomes — never queries or responses). Three ways to opt out, any of them works:
AgentKey sends anonymous usage telemetry (which agent you install into or remove from, skill version, upgrade outcomes — never queries or responses). Three ways to opt out, any of them works:

```bash
# Persistent opt-out (recommended)
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ The skill verifies MCP health by calling the MCP `list_tools` endpoint directly
|---|---|---|
| `api.github.com` | At most every 24 hours | Look up the latest release tag |
| npm registry | When the user accepts a skill update | Resolve and run `npx skills update agentkey` |
| `api.agentkey.app` | On install, uninstall, and best-effort skill/update checks unless telemetry is disabled | Anonymous usage telemetry and update metadata |

### Credential handling

- `AGENTKEY_API_KEY` is stored only in user-local config files (paths above).
- The key leaves the user's machine only as the `Authorization` header to AgentKey's own API endpoints.
- The skill collects no telemetry.
- Telemetry is anonymous and limited to install/uninstall/update metadata; it never includes queries, responses, or local file contents.

### Supply chain

Expand Down
4 changes: 2 additions & 2 deletions docs/README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ AgentKey 在云端维护与开放互联网各类平台的对接 —— 你不需
<details>
<summary><b>安全吗?</b></summary>

安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。
安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到或移除了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。

</details>

Expand Down Expand Up @@ -230,7 +230,7 @@ npx skills remove chainbase-labs/agentkey
<details>
<summary><b>我如何关闭遥测?</b></summary>

AgentKey 会上报匿名使用统计(你用的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭:
AgentKey 会上报匿名使用统计(你安装到或移除的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭:

```bash
# 持久关闭(推荐)
Expand Down
66 changes: 64 additions & 2 deletions scripts/dev-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,11 @@ phase_4() {
cat > "$SANDBOX/.cursor/mcp.json" <<'EOF'
{
"mcpServers": {
"agentkey": { "command": "npx", "args": ["-y", "@agentkey/cli"] },
"agentkey": {
"command": "npx",
"args": ["-y", "@agentkey/cli"],
"headers": { "Authorization": "Bearer ${user_config.AGENTKEY_API_KEY}" }
},
"agentkey-helper": { "command": "x" },
"other-svr": { "command": "y" }
}
Expand Down Expand Up @@ -390,6 +394,48 @@ command = "legacy"
key = "val"
EOF

# Mock telemetry endpoint: uninstaller should post once before scrubbing
# configs, but telemetry must remain best-effort and isolated to sandbox.
local telemetry_log="$SANDBOX/telemetry.json"
local telemetry_port_file="$SANDBOX/telemetry-port"
python3 -u - "$telemetry_log" "$telemetry_port_file" <<'PY' &
import http.server
import json
import sys

log_path, port_path = sys.argv[1], sys.argv[2]

class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length") or "0")
body = self.rfile.read(length).decode("utf-8")
with open(log_path, "w", encoding="utf-8") as f:
f.write(json.dumps({
"path": self.path,
"authorization": self.headers.get("Authorization"),
"body": body,
}))
self.send_response(204)
self.end_headers()

def log_message(self, *_):
pass

server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
with open(port_path, "w", encoding="utf-8") as f:
f.write(str(server.server_port))
server.timeout = 10
server.handle_request()
PY
local telemetry_pid=$!
local _wait
for _wait in {1..50}; do
[ -s "$telemetry_port_file" ] && break
sleep 0.1
done
local telemetry_port
telemetry_port="$(cat "$telemetry_port_file" 2>/dev/null || true)"

# Gemini: claude-code per-project shape simulator (nested).
cat > "$SANDBOX/.claude.json" <<'EOF'
{
Expand All @@ -407,8 +453,24 @@ EOF

# ── Run the uninstaller in the sandbox ────────────────────────────────
info "running uninstall.sh --skip-skill-remove --force-in-repo"
HOME="$SANDBOX" bash "$SKILL_REPO/scripts/uninstall.sh" \
AGENTKEY_BASE_URL="http://127.0.0.1:$telemetry_port" \
HOME="$SANDBOX" bash "$SKILL_REPO/scripts/uninstall.sh" \
--skip-skill-remove --force-in-repo >/dev/null 2>&1 || true
for _wait in {1..50}; do
[ -s "$telemetry_log" ] && break
sleep 0.1
done
kill "$telemetry_pid" 2>/dev/null || true
wait "$telemetry_pid" 2>/dev/null || true

local telemetry_body
telemetry_body="$(cat "$telemetry_log" 2>/dev/null || true)"
assert_contains "uninstall telemetry endpoint called" \
"/v1/telemetry/uninstall-completed" "$telemetry_body"
assert_contains "uninstall telemetry uses stored API key" \
"Bearer ak_xxx" "$telemetry_body"
assert_contains "uninstall telemetry includes removed codex agent" \
"codex" "$telemetry_body"

# ── Assertions: agentkey gone, decoys preserved ───────────────────────
info "checking: agentkey entries scrubbed"
Expand Down
174 changes: 172 additions & 2 deletions scripts/uninstall.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,176 @@ function Write-Warn2($t) { Write-Host " ! $t" -ForegroundColor Yellow }
function Write-Skip ($t) { Write-Host " - $t" -ForegroundColor DarkGray }
function Write-Err ($t) { Write-Host " ✗ $t" -ForegroundColor Red }

$home2 = [Environment]::GetFolderPath('UserProfile')
$UninstallerFlags = @($PSBoundParameters.Keys | ForEach-Object { "-$_" })

function Send-UninstallTelemetry {
try {
if ($env:AGENTKEY_TELEMETRY -eq '0') { return }
$optOut = Join-Path $home2 '.config\agentkey\telemetry-disabled'
if (Test-Path -LiteralPath $optOut) { return }

$script:TelemetryApiKey = $null
$script:TelemetryAgents = @()
$serverNames = @('agentkey', 'agentkey.app agentkey')

function Add-TelemetryAgent($id) {
if ($script:TelemetryAgents -notcontains $id) {
$script:TelemetryAgents += $id
}
}

function Set-TelemetryApiKey($value) {
if ($null -eq $value) { return $false }
$v = ([string]$value).Trim()
if (-not $v.StartsWith('ak_')) { return $false }
$script:TelemetryApiKey = $v
return $true
}

function Read-TelemetryEntry($id, $entry) {
Add-TelemetryAgent $id
if ($null -eq $entry -or $script:TelemetryApiKey) { return }
try {
foreach ($field in @('headers', 'http_headers', 'env', 'environment')) {
$bagProp = $entry.PSObject.Properties[$field]
if ($null -eq $bagProp -or $null -eq $bagProp.Value) { continue }
$bag = $bagProp.Value
if ($bag.PSObject.Properties['Authorization']) {
$auth = [string]$bag.Authorization
if ($auth -match '(?i)\bBearer\s+(.+)$') {
if (Set-TelemetryApiKey $Matches[1]) { return }
}
}
if ($bag.PSObject.Properties['AGENTKEY_API_KEY']) {
if (Set-TelemetryApiKey $bag.AGENTKEY_API_KEY) { return }
}
}
} catch {}
}

function Walk-TelemetryJson($id, $node) {
if ($null -eq $node) { return }
if ($node -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in @($node.PSObject.Properties)) {
if ($serverNames -contains $prop.Name.ToLower()) {
Read-TelemetryEntry $id $prop.Value
} else {
Walk-TelemetryJson $id $prop.Value
}
}
} elseif ($node -is [System.Collections.IEnumerable] -and -not ($node -is [string])) {
foreach ($item in $node) { Walk-TelemetryJson $id $item }
}
}

$jsonConfigs = @(
@{ Id = 'claude-code'; Path = (Join-Path $home2 '.claude.json') },
@{ Id = 'cursor'; Path = (Join-Path $home2 '.cursor\mcp.json') },
@{ Id = 'claude-desktop'; Path = (Join-Path $env:APPDATA 'Claude\claude_desktop_config.json') },
@{ Id = 'gemini-cli'; Path = (Join-Path $home2 '.gemini\settings.json') },
@{ Id = 'qwen-code'; Path = (Join-Path $home2 '.qwen\settings.json') },
@{ Id = 'iflow-cli'; Path = (Join-Path $home2 '.iflow\settings.json') },
@{ Id = 'kimi-cli'; Path = (Join-Path $home2 '.kimi\mcp.json') },
@{ Id = 'kiro-cli'; Path = (Join-Path $home2 '.kiro\settings\mcp.json') },
@{ Id = 'windsurf'; Path = (Join-Path $home2 '.codeium\windsurf\mcp_config.json') },
@{ Id = 'warp'; Path = (Join-Path $home2 '.warp\.mcp.json') },
@{ Id = 'opencode'; Path = (Join-Path $env:APPDATA 'opencode\opencode.json') },
@{ Id = 'amp'; Path = (Join-Path $env:APPDATA 'amp\settings.json') },
@{ Id = 'crush'; Path = (Join-Path $env:APPDATA 'crush\crush.json') }
)
foreach ($cfg in $jsonConfigs) {
if (-not (Test-Path -LiteralPath $cfg.Path)) { continue }
try {
Walk-TelemetryJson $cfg.Id (Get-Content $cfg.Path -Raw | ConvertFrom-Json)
} catch {}
}

$codexToml = Join-Path $home2 '.codex\config.toml'
if (Test-Path -LiteralPath $codexToml) {
$inBlock = $false
foreach ($line in Get-Content $codexToml) {
if ($line -match '^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")(\.[^\]]+)?\s*\]\s*$') {
$inBlock = $true
Add-TelemetryAgent 'codex'
continue
}
if ($line -match '^\s*\[[^\]]+\]\s*$') { $inBlock = $false }
if (-not $inBlock -or $script:TelemetryApiKey) { continue }
if ($line -match 'Authorization\s*=\s*"Bearer\s+([^"]+)"') {
Set-TelemetryApiKey $Matches[1] | Out-Null
} elseif ($line -match 'AGENTKEY_API_KEY\s*=\s*"([^"]+)"') {
Set-TelemetryApiKey $Matches[1] | Out-Null
}
}
}

if (-not $script:TelemetryApiKey -or $script:TelemetryAgents.Count -eq 0) { return }

$skillVersion = ''
$versionCandidates = @(
'.agents\skills\agentkey\version.txt',
'.claude\skills\agentkey\version.txt',
'.cursor\skills\agentkey\version.txt',
'.codex\skills\agentkey\version.txt',
'.gemini\skills\agentkey\version.txt',
'.opencode\skills\agentkey\version.txt',
'.openclaw\skills\agentkey\version.txt',
'.qwen\skills\agentkey\version.txt',
'.iflow\skills\agentkey\version.txt',
'.windsurf\skills\agentkey\version.txt',
'.warp\skills\agentkey\version.txt',
'.kimi\skills\agentkey\version.txt',
'.kiro\skills\agentkey\version.txt'
)
foreach ($rel in $versionCandidates) {
$p = Join-Path $home2 $rel
if (Test-Path -LiteralPath $p) {
$skillVersion = (Get-Content $p -Raw).Trim()
break
}
}
foreach ($rel in @(
'amp\skills\agentkey\version.txt',
'crush\skills\agentkey\version.txt',
'goose\skills\agentkey\version.txt',
'opencode\skills\agentkey\version.txt'
)) {
if ($skillVersion) { break }
$p = Join-Path $env:APPDATA $rel
if (Test-Path -LiteralPath $p) {
$skillVersion = (Get-Content $p -Raw).Trim()
}
}

$_hn = [System.Net.Dns]::GetHostName()
$_user = $env:USERNAME
$_bytes = [System.Text.Encoding]::UTF8.GetBytes("$_hn|windows|$_user")
$_sha = [System.Security.Cryptography.SHA256]::Create()
$_hash = ($_sha.ComputeHash($_bytes) | ForEach-Object { $_.ToString('x2') }) -join ''
$baseUrl = if ($env:AGENTKEY_BASE_URL) { $env:AGENTKEY_BASE_URL.TrimEnd('/') } else { 'https://api.agentkey.app' }
$payload = @{
properties = @{
device_fingerprint = $_hash.Substring(0, 16)
uninstall_source = 'one_liner'
removed_agents = @($script:TelemetryAgents | Sort-Object -Unique)
uninstaller_flags = $script:UninstallerFlags
skill_version = $skillVersion
os = 'win32'
os_arch = $env:PROCESSOR_ARCHITECTURE
}
} | ConvertTo-Json -Depth 10

Invoke-WebRequest -Uri "$baseUrl/v1/telemetry/uninstall-completed" `
-Method Post `
-Headers @{ Authorization = "Bearer $script:TelemetryApiKey" } `
-ContentType 'application/json' `
-Body $payload `
-TimeoutSec 2 `
-UseBasicParsing | Out-Null
} catch {}
}

# ── Safety rail ──────────────────────────────────────────────────────────
if ((Test-Path '.claude-plugin/plugin.json') -and -not $ForceInRepo) {
$content = Get-Content '.claude-plugin/plugin.json' -Raw -ErrorAction SilentlyContinue
Expand All @@ -68,6 +238,8 @@ Write-Host ''
Write-Host ' AgentKey — Uninstall' -ForegroundColor White
Write-Host ' https://agentkey.app' -ForegroundColor DarkGray

Send-UninstallTelemetry

# ── 1. Skill removal via skills CLI ──────────────────────────────────────
Write-Step '1. Skill files'

Expand All @@ -94,8 +266,6 @@ if ($SkipSkillRemove) {
# ── 2. MCP config cleanup ────────────────────────────────────────────────
Write-Step '2. MCP server entries'

$home2 = [Environment]::GetFolderPath('UserProfile')

# All known JSON MCP config paths across the 16 auto-supported agents. The
# scrub logic is schema-agnostic — it walks the JSON tree and drops any
# dict key whose name exactly matches our server name (current + legacy),
Expand Down
Loading
Loading