返回 Skill 列表
extension
分类: 开发与工程无需 API Key

密钥管理

让 AI Agent 通过系统原生密钥存储(macOS 钥匙串 / Windows 凭据管理器)安全管理 API 密钥:交互式存入(脱敏输入、先查后写)、路由表查询、三元组确认删除、跨机一键注入脚本,另附 Python/Node/Go/Rust 程序端取值三模式。Agent 全程接触不到密钥明文——密钥只经用户的手和系统加密层,不落任何明文文件。经五轮对抗性审查收敛、macOS 全链路真机实测。注*输入时无法看见所输入的内容,可以剪贴板复制进去

person作者: AllenChen0318hubModelScope

跨平台密钥管理(macOS 钥匙串 / Windows 凭据管理器)

平台识别(每个流程的第一步)

| 平台 | 判断方式 | 存储后端 | |------|----------|----------| | macOS | uname 输出 Darwin | 钥匙串(security 命令) | | Windows | 首选 Get-Command cmdkey -ErrorAction SilentlyContinue 存在(兼容 PS5.1 与 PS7;$IsWindows 仅 PS6+ 可用,禁止作为唯一判据) | 凭据管理器(cmdkey) | | Linux | 以上均不满足 | 不支持,明确告知用户并停止 |

术语映射(两个平台概念对齐):

| 概念 | macOS | Windows | |------|-------|---------| | 账户名 | -a | /user: | | 密钥名 | -s | /generic:(target) |

安全铁律(任何流程中必须遵守)

  1. 禁止 Agent 执行任何会输出密钥明文的命令(mac 的 find-generic-password ... -w、win 的 SecureString 解密命令、echo $变量 等),取值命令只展示给用户自行执行。
  2. 存入流程中密钥值只由用户在终端输入,Agent 生成的命令中一律用占位符,禁止出现真实密钥。
  3. 给第三方(如 Quest)交接时,只传递取值命令,不传递密钥值。

命令速查(显式命令)

macOS(Keychain)

| 操作 | 命令 | |------|------| | 写入(新增) | security add-generic-password -a <账户名> -s <密钥名> -w "$SK_VALUE" | | 写入(覆盖已有) | security add-generic-password -a <账户名> -s <密钥名> -w "$SK_VALUE" -U | | 查询元数据(不含密钥值) | security find-generic-password -a <账户名> -s <密钥名> | | 获取密钥值(仅用户在终端自行执行) | security find-generic-password -a <账户名> -s <密钥名> -w | | 列出全部条目(仅流程二未命中时的辅助扫描;流程一候选只认路由表,禁止用本命令找候选) | security dump-keychain \| grep -E '"acct"<blob>\|"svce"<blob>' \| sort -u | | 删除 | security delete-generic-password -a <账户名> -s <密钥名> |

Windows(Credential Manager)

| 操作 | 命令 | |------|------| | 写入(省略 /pass 触发交互式掩码输入;同名自动覆盖) | cmdkey /generic:<密钥名> /user:<账户名> | | 查询列表(不含密钥值) | cmdkey /listcmdkey /list:<密钥名> | | 获取密钥值(仅用户自行执行,需 CredentialManager 模块) | 见流程二 | | 删除 | cmdkey /delete:<密钥名> | | 安装取值模块(一次性,免管理员;-Force 跳过 PS5.1 首次使用的 NuGet provider 交互确认,避免非交互会话卡死) | Install-Module -Name CredentialManager -Scope CurrentUser -Force |

注意:cmdkey 本身读不出明文,Windows 端取值必须依赖 CredentialManager 模块。

流程一:存入新密钥

执行纪律(最简路径,用户已确认):本流程必须通过 Skill 工具调用后严格按步骤执行——禁止附加本流程未写的探索动作(尤其禁止在步骤 2 用 dump-keychain 扫描钥匙串找候选,候选只认路由表);汇报从简,过程性步骤不做总结表格。完整路径:平台识别 → 面板选账户名 → 直接填密钥名 → 查重 → 开终端输入 → 确认面板 → 存在性验证 → 登记路由表。

步骤 1 — 平台识别(显式执行,不可跳过)

先按「平台识别」表判断当前运行平台,后续所有步骤按对应平台分支执行。

步骤 2 — 确定账户名(面板第一问:从已有 -a 名中选择)

候选来源只认本文件密钥路由表中已登记的账户名(-a)(禁止把系统钥匙串/凭据管理器里的无关条目当候选——mac 的 WiFi/应用密码、win 的浏览器凭据都是噪音),选项直接逐个列出这些已登记的 -a 名;路由表为空则视为无候选。

所有提问必须通过 AskUserQuestion 交互提问模块进行(禁止纯文本罗列问题),选项只放实质候选值,AskUserQuestion 自带 Other 自定义输入入口,禁止额外设置「选 Other 输入」之类的占位选项:

  • 有候选账户名:选项为复用已有账户名(逐个列出)、当前系统用户名(mac 为 $USER、win 为 $env:USERNAME,标推荐)。
  • 无候选账户名:选项至少两个——当前系统用户名(标推荐)、default,需要自定义时由工具自带的 Other 入口输入。

账户名拿到后先做格式校验(与密钥名同规则:只允许 [a-zA-Z0-9_-],长度 ≤64)——账户名会裸插值进步骤 4 的命令字符串,含空格/引号/分号/中文会切分参数、破坏引号结构甚至注入命令,校验不通过必须拒绝并要求重新输入。钥匙串/凭据管理器中已存在的含空格/中文等历史账户名同样过不了校验、不可复用——这是防命令注入的有意取舍,遇到此类情况向用户说明原因,并引导其新建一个合规账户名。

步骤 3 — 确定密钥命名(第二问)

密钥名由用户直接填写,禁止提供任何预设示例选项、命名格式建议或解说(用户已确认)。提问形态:若能从用户当前消息上下文推断出实质候选名(如用户原话提到具体服务),用 AskUserQuestion 与步骤 2 一轮收齐、选项只列该推断候选;无法推断时用纯文本直接请用户输入密钥名(这是提问面板纪律的唯一豁免,因面板机制要求 2-4 个实质选项而新密钥名无实质候选源)。 拿到密钥命名后先做格式校验(只允许 [a-zA-Z0-9_-],长度 ≤64,拒绝含 /\:、空格、中文等特殊字符的命名——cmdkey 和 security 对此类字符解析行为不可靠),再查重:

  • macOS:用查询元数据命令检查,已存在则用 AskUserQuestion 询问「覆盖(加 -U,旧密钥将被销毁)还是换名」,必须等用户明确选择后才继续
  • Windows:cmdkey /list:<密钥名> 检查,已存在时必须先用 AskUserQuestion 向用户明确「同名覆盖将销毁旧密钥值且不可恢复」并等待用户确认覆盖或换名——cmdkey 同名写入是静默覆盖,禁止仅告知不等待确认就继续

步骤 4 — 打开终端,预置输入命令(按平台分支)

macOS:先用 Write 工具生成临时 AppleScript 文件(单行 osascript -e 的引号转义不可靠,必须用文件方式),把 <账户名><密钥名> 替换为已确认的值(二者均通过步骤 2/3 的 [a-zA-Z0-9_-] 校验,可安全插值;账户名与密钥名一律用 quoted form of 包裹以防未来放宽校验时引入注入)。覆盖开关:步骤 3 用户选择「覆盖」时,把模板中 set uOpt to "" 改为 set uOpt to " -U"(注意前导空格);新增或换名则保持空串。 必须用 /bin/bash -c 包裹整条命令(真机实证:Terminal 打开的是用户默认 shell,zsh 的 read 不支持 -p 会报 -p: no coprocess,命令首步即失败且用户可能把密钥当命令敲入命令行造成泄露)。 禁止改用 security add-generic-password ... -w 不带值的自带提示来简化模板(真机实证:自带提示要求输入两次——password + retype 确认,粘贴长密钥易错、两次不一致陷入重输循环,且掩码完全静默无任何字符反馈,可用性不可接受):

set acct to quoted form of "<账户名>"
set skName to quoted form of "<密钥名>"
set uOpt to ""
set innerCmd to "read -s -p " & quoted form of "请输入密钥(输入不回显),按回车存入: " & " SK_VALUE && echo && if security add-generic-password -a " & acct & " -s " & skName & uOpt & " -w \"$SK_VALUE\"; then unset SK_VALUE; echo \"[成功] 已存入钥匙串: <密钥名>\"; else unset SK_VALUE; echo \"[失败] 存入失败,请把上方错误信息反馈给 Agent\"; fi"
set shellCmd to "/bin/bash -c " & quoted form of innerCmd
tell application "Terminal"
	activate
	do script shellCmd
end tell

然后执行 osascript <临时文件路径>,并删除该临时文件。 存入成功的标志是终端显示「[成功] 已存入钥匙串: <密钥名>」;失败时终端显示 [失败],用户把上方错误信息原文反馈给 Agent(由错误信息本身定位原因,不做预先归因),与 Windows 分支反馈对齐。

Windows:打开新 PowerShell 窗口预置命令(省略 /pass,cmdkey 会自动进入掩码密码输入提示)。

语法要求(高危修正):-ArgumentList 必须拼接为单个字符串,禁止逗号分隔数组写法;命令体中的 ; 使用裸分号(外层单引号字符串中 ; 无特殊含义直接穿透,禁止用反引号转义——反引号会把 ; 变成 cmdkey 的字面量参数);用 $LASTEXITCODE 区分成败,禁止无条件输出成功提示:

Start-Process powershell -ArgumentList ('-NoExit -Command "cmdkey /generic:<密钥名> /user:<账户名>; if ($LASTEXITCODE -eq 0) { Write-Host ''[成功] 已存入凭据管理器,可关闭本窗口'' } else { Write-Host ''[失败] 存入失败,请把上方错误信息反馈给 Agent'' }"')

窗口中 cmdkey 提示输入密码时(中文系统提示语可能为「输入 <密钥名> 的密码:」或类似文本,勿依赖固定字符串判断),用户粘贴密钥并回车。 窗口会按真实退出码显示 [成功][失败],用户把结果反馈给 Agent。

两个平台执行后都明确告知用户:已打开终端窗口,请在其中粘贴密钥并按回车,然后把窗口显示的成功/失败结果告诉我

步骤 5 — 结果确认 + 存在性验证 + 登记密钥路由(先查后写,禁止跳过验证)

步骤 4 打开终端后,立即用 AskUserQuestion 向用户确认终端显示的结果(选项只放实质候选:「显示 [成功]」/「显示 [失败] 或异常」,失败时由工具自带的 Other 入口粘贴错误信息原文)。用户点击确认即完成输入的信号,此后 Agent 必须先执行存在性检查(不输出密钥值,可安全代跑),验证通过才登记路由表,根治幽灵条目:

  • macOS:security find-generic-password -a <账户名> -s <密钥名>,退出码 0 即存在
  • Windows:cmdkey /list:<密钥名>,输出含该目标即存在

验证失败时:明确告知用户密钥未实际写入,请重新执行步骤 4,禁止登记路由表。 验证通过后,更新本文件末尾的「密钥路由表」(必须遵守以下原子性规则):

  1. 先用 Read 工具完整读取当前路由表,确认无同平台同密钥名的重复行;
  2. 若表中仍存在占位行 (暂无,存入后自动登记),追加真实数据行的同时删除该占位行(这是唯一允许修改既有行的场景);
  3. 用 SearchReplace 精确替换完成追加:以表尾内容为搜索目标,替换内容必须是「原表尾内容 + 新行」——原有行必须原样保留,禁止任何会覆盖/丢失既有数据行的盲写;追加行注明平台,禁止记录密钥值本身。

流程二:查询 / 获取密钥

步骤 1 — 平台识别与跨平台校验:先执行平台识别;再根据路由表中目标条目的平台列判断:条目平台与当前平台不一致时,明确告知用户「该密钥存于 {另一平台},本机无法操作」,禁止执行当前平台命令去查找。 步骤 2 — 匹配条目:查本文件「密钥路由表」,按用户描述的名称或服务匹配条目(注意平台列)。 步骤 3 — 扫描候选:路由表未命中时,按平台运行列出命令扫描候选(仅展示,不代跑取值)。 步骤 4 — 验证与取值:命中后验证条目存在(mac 查询元数据 / win cmdkey /list:<密钥名>,均不输出密钥值,Agent 可安全执行)。获取密钥值时禁止 Agent 代跑:把取值命令展示给用户自行执行。

macOS 取值命令:

security find-generic-password -a <账户名> -s <密钥名> -w

若不想明文显示在屏幕上(防旁观/录屏/滚动缓冲区残留),用剪贴板版——值直接进剪贴板、屏幕无任何明文输出(注意剪贴板不加密、其他应用可读,用后及时复制其他内容覆盖):

security find-generic-password -a <账户名> -s <密钥名> -w | pbcopy && echo "已复制到剪贴板"

Windows 取值命令(先确认模块已安装:Get-Module -ListAvailable CredentialManager,缺失则引导用户执行安装命令)。

兼容性要求(高危修正):取值命令必须带空值保护——Get-StoredCredential 对 cmdkey 写入的 generic credentials 在不同 Windows/PowerShell 版本上行为有差异,返回空时禁止直接访问 .Password(会抛空引用异常):

$c = Get-StoredCredential -Target '<密钥名>'
if ($null -eq $c) { Write-Host '未找到该凭据,请确认密钥名拼写与 CredentialManager 模块版本'; exit 1 }
[Runtime.InteropServices.Marshal]::PtrToStringUni([Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($c.Password))

若取值失败(模块读不到 cmdkey 写入的条目),降级路径只有一条:执行 Update-Module CredentialManager 升级模块后重试;仍失败则将现象反馈给技能维护者,禁止援引任何未经真机验证的第三方替代库,禁止静默放弃。

步骤 5 — 脚本/程序注入方式:Agent 只写入「取值命令模板」进脚本(密钥值由运行时展开,Agent 不代跑展开命令,避免密钥明文落盘到脚本文件)。禁止把密钥值写入 .env 等明文文件——「部署前转存 .env」同样禁止,明文落盘正是本技能要消除的状态(会被 git 提交、备份、共享读取扩散);若部署平台只认 .env 文件,由用户自行在本地执行取值命令生成该文件并加入 .gitignore,Agent 不代跑。另注意:环境变量注入在多用户共享机器上禁止使用(进程环境变量可被任务管理器/其他进程读取)——共享机器场景下改为由用户在启动程序前手动执行取值命令并自行传参,技能不提供自动注入:

macOS(取值失败即退出,禁止空密钥继续执行):

MY_SK="$(security find-generic-password -a <账户名> -s <密钥名> -w)" || { echo "密钥读取失败"; exit 1; }
export MY_SK

Windows(PowerShell,同样带空值保护):

$c = Get-StoredCredential -Target '<密钥名>'
if ($null -eq $c) { Write-Host '密钥读取失败'; exit 1 }
$env:MY_SK = [Runtime.InteropServices.Marshal]::PtrToStringUni([Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($c.Password))

步骤 6 — 跨机/人为部署(exe 到另一台机器使用、服务器持久运行):钥匙串/凭据管理器是本机设施,目标机读不到开发机的密钥,密钥必须在目标端配置。本技能提供一键部署注入脚本(技能 scripts/ 目录),在目标机运行,掩码输入直接写入系统凭据库:

  • macOS:./inject-sk.sh <密钥名> <账户名>
  • Windows:powershell -ExecutionPolicy Bypass -File inject-sk.ps1 -KeyName <密钥名> -AccountName <账户名>

脚本行为统一:格式校验([a-zA-Z0-9_-] ≤64)→ 查重并询问覆盖确认 → 掩码输入 → 写入(mac security ... -U / win cmdkey,win 端写入后自动存在性验证)→ 按退出码反馈 [成功]/[失败]。把脚本拷到目标机后由用户自行运行,Agent 不代跑输入环节。 程序端取值:手工 shell 启动场景用上方步骤 5 的运行时展开片段(mac bash / win PowerShell),密钥仅启动瞬间展开;程序代码内的双端取值方案(启动器脚本 / keyring 库 / 子进程 shell-out 三种模式,含 Python/Node/Go/Rust 示例)见 deployment-code.md。注意:系统凭据库依赖交互式会话,无登录会话的 headless 服务不适用此路径headless / .env 替代路径:无交互会话的服务或只认 .env 的部署平台改用此路径——用户在开发机终端自行执行取值命令拿到值 → 在目标机程序旁创建 .env(内容 MY_SK=<值>)→ 文件权限 600、加入 .gitignore;程序端建议「环境变量优先、.env 兜底」双层读取(各生态有成熟 dotenv 库),未来接入 CI/CD secrets / systemd EnvironmentFile= / Docker secrets 时环境变量直接就位、删 .env 即可,零代码改动。密钥可以经人之手,禁止经 git、聊天记录、可复制文档流转;Agent 全程不代跑取值、不代写该文件。

流程三:删除密钥

步骤 1 — 平台识别与跨平台校验(同流程二步骤 1,跨平台条目拒绝本机删除)。 步骤 2 — 二次确认:用 AskUserQuestion 向用户确认目标条目,确认选项必须形如「平台 / 密钥名 / 账户名」完整三元组,防止同名跨平台条目误删。 步骤 3 — 执行删除并同步路由表:执行删除命令,并用 SearchReplace 删除本文件密钥路由表中对应行。若删除的是表中最后一条数据行(删后仅剩表头),必须恢复占位行 (暂无,存入后自动登记),保证「路由表为空」状态判断始终有唯一依据。

  • macOS:security delete-generic-password -a <账户名> -s <密钥名>
  • Windows:cmdkey /delete:<密钥名>

密钥路由表

并发约定(用户已确认):本表按单会话维护——密钥存入/删除为低频操作,不引入锁机制。禁止多个会话/Quest 同时修改本文件;任何会话修改路由表前必须先 Read 最新表内容,以读到的内容为替换基准(配合流程一步骤 5 的原子性规则)。

| 平台 | 密钥名 | 账户名 | 存入日期 | 取值命令 | |------|--------|--------|----------|----------| | (暂无,存入后自动登记) | | | | |