这篇能做出什么
一句话:让 Agent 替你登录某个第三方系统(公司 staging 后台、供应商门户、内部工单系统都行),拿到登录后的会话去干活,而明文密码全程不出现在模型上下文、对话记录、命令行回显和日志里。
做完之后有三条可以当场验收:
1. Agent 能调用一个叫 login_to_portal 的工具,参数只有一个 target,schema 里根本没有密码字段;
2. 工具返回的只有 {"ok": true, "url": "..."} 这样的状态,密码不进返回值;
3. 你在 1Password 里把密码改掉,Agent 一行代码都不用动,下次调用自动用上新密码。
再加一条运维视角的:每次凭据读取都能在 1Password 的活动记录里对上账,怀疑出事时能一键吊销 Agent 的访问权,而不是满服务器找 .env。
核心思路是把「读密码」和「用密码」压进同一个本地进程里,模型只能看到「登录成功/失败」这个结果。密码从保险库出来,直接进浏览器输入框,中间不落地。
前置条件清单
- 一个 1Password 账号。个人版、家庭版、团队版都能跑通下面的流程;Service Account 这类无人值守凭据需要团队/企业版套餐,是否可用以及具体配额以官方文档当前版本为准。
- 1Password 桌面 App(用于首次授权 CLI)或 Service Account 令牌。
- 1Password CLI(
op),安装方式见官方文档,装完用op --version确认能跑。 - Node.js 18 以上(脚本用 ESM + Playwright)。
- 一个专门用来练手的第三方账号,别一上来就拿生产环境管理员账号试。
- 一个能跑 Playwright 的机器,
npx playwright install chromium能装上浏览器。
建议先在本地笔记本跑通,再搬到服务器上。
步骤一:建一个只放 Agent 凭据的专用保管库
不要把 Agent 塞进你现有的「个人」或「团队共享」库里。新建一个独立的库,目的很单纯:这个库里所有东西,都是允许 Agent 读的;反过来,Agent 能读到的也只有这个库。
在 1Password App 里新建保管库,命名 Agent-Login,然后在这个库里新建一个「登录」类型的条目:
- 标题:
staging-portal - 网址:
https://staging.example.com/login - 用户名:
agent-bot(给 Agent 单独开一个账号,不要用你自己的) - 密码:用 App 的随机密码生成器生成
- 如果站点开了两步验证,把 TOTP 种子也存进这个条目的一次性密码字段
建完用 CLI 验证一下条目结构,字段名后面写代码时要对齐:
```bash
op signin # 首次授权,会拉起桌面 App 确认
op vault list # 确认能看到 Agent-Login
op item list --vault "Agent-Login"
op item get "staging-portal" --vault "Agent-Login" --format json
```
--format json 里会列出每个字段的 id 和 label。记住密码字段的写法(一般是 password)和一次性密码字段的写法,下面拼 op:// 引用时要用。不要加 --reveal,那会把明文打到终端和 shell 历史里。
顺手试一下引用语法:
```bash
op read "op://Agent-Login/staging-portal/password" > /dev/null && echo "引用可用"
```
能打印「引用可用」就说明路径对了。一次性验证码的引用通常是:
```bash
op read "op://Agent-Login/staging-portal/one-time password?attribute=otp"
```
字段名以你条目里实际的字段名为准,写错了 op 会直接报找不到。
步骤二:选一条授权路线,把令牌交给 Agent 主机
有两条路,按场景选:
路线 A:桌面 App 授权(适合单机、开发和人工值守场景)
op signin 之后,CLI 通过桌面 App 拿到一个会话,App 锁屏或退出后需要重新授权。优点是不用额外配置,缺点是机器上必须有人登录着 App,无人值守跑不通。
路线 B:Service Account(适合服务端、无人值守)
在 1Password 后台创建 Service Account,权限只勾选 Agent-Login 这一个库、只给读取权限。创建完会拿到一个令牌,只显示一次。把它放到 Agent 主机的环境变量里:
```bash
不要写进代码仓库,不要写进 .env 后提交
export OP_SERVICE_ACCOUNT_TOKEN="<粘贴令牌>"
op vault list # 用令牌身份验证,应该只能看到 Agent-Login
```
这一步的验证很关键:用令牌跑 op vault list,如果能看到别的库,说明权限给大了,回去收窄。同理,用令牌跑 op item edit 应该失败——Agent 只需要读,读不到改的权限才安全。
令牌本身是高价值凭据,建议放在系统的密钥环或专用的 secrets 注入机制里,文件权限设 600,并纳入定期轮换计划。
步骤三:写一个「凭据代理」进程
这是整条链路的关键。密码只能出现在这个进程的内存里,不能出现在它的 stdout 里。
新建目录,装依赖:
```bash
mkdir agent-login-broker && cd agent-login-broker
npm init -y
npm pkg set type=module
npm i playwright
npx playwright install chromium
```
创建 broker.js:
```javascript
// broker.js —— 凭据只在这个进程内部流转,绝不写日志、绝不进返回值
import { execFileSync } from 'node:child_process';
import { chromium } from 'playwright';
const VAULT = 'Agent-Login';
// 目标白名单:Agent 只能登录这里列出的站点,写死,不接受运行时拼装
const TARGETS = {
'staging-portal': {
item: 'staging-portal',
url: 'https://staging.example.com/login',
userSelector: 'input[name="username"]',
passSelector: 'input[name="password"]',
otpSelector: 'input[name="otp"]', // 站点没开两步验证就删掉相关逻辑
submitSelector: 'button[type="submit"]',
successCheck: (page) => !page.url().includes('/login'),
},
};
// 唯一读取凭据的入口:用子进程读,结果只回到本进程
function readSecret(item, field) {
return execFileSync('op', ['read', op://${VAULT}/${item}/${field}], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'], // 别继承父进程的 stdout
env: process.env, // 令牌从环境变量继承
}).trim();
}
export async function loginToPortal(target) {
const cfg = TARGETS[target];
if (!cfg) return { ok: false, reason: 'target_not_allowed' };
// 现取现用,不缓存到模块级变量
const username = readSecret(cfg.item, 'username');
const password = readSecret(cfg.item, 'password');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(cfg.url, { waitUntil: 'domcontentloaded' });
await page.fill(cfg.userSelector, username);
await page.fill(cfg.passSelector, password);
if (cfg.otpSelector) {
// 一次性验证码时效很短,必须登录前现取
await page.fill(cfg.otpSelector, readSecret(cfg.item, 'one-time password?attribute=otp'));
}
await page.click(cfg.submitSelector);
await page.waitForLoadState('networkidle');
const ok = cfg.successCheck(page);
// 会话落盘等价于登录态,权限必须收紧
await context.storageState({ path: /var/lib/agent/session-${target}.json });
// 只回状态,永远不把 username / password 放进返回值
return { ok, url: page.url() };
} catch (err) {
// 只回错误类型,避免异常对象被序列化后带出上下文
return { ok: false, reason: 'login_flow_error', detail: String(err.name) };
} finally {
await browser.close();
}
}
```
跑一下自测,确认能登录:
```bash
node -e "import('./broker.js').then(async m => console.log(await m.loginToPortal('staging-portal')))"
```
预期输出类似 { ok: true, url: 'https://staging.example.com/dashboard' }。看到密码就说明哪一步写错了,回去查。
步骤四:把工具暴露给 Agent,schema 里不留密码位
无论你用的是哪个 Agent 框架,工具定义只暴露 target,enum 里只放白名单:
```json
{
"name": "login_to_portal",
"description": "登录指定的第三方后台并返回是否成功。不返回任何凭据,不要尝试询问密码。",
"parameters": {
"type": "object",
"properties": {
"target": { "type": "string", "enum": ["staging-portal"] }
},
"required": ["target"]
}
}
```
处理函数就是:
```javascript
const result = await loginToPortal(args.target);
return JSON.stringify(result); // 只有 ok / url / reason
```
这里有三个必须守住的边界:
- 不提供通用 shell 工具。一旦 Agent 能执行任意命令,它就能跑
op read把密码打出来,前面的封装全白做。 - 不提供通用
op包装工具。不要让模型决定读哪个 vault、哪个字段。 - 系统提示里写清楚:凭据由本地代理处理,模型不需要也不应该知道密码,遇到需要密码的环节就调用
login_to_portal。
步骤五:加一道人工审批
凭据读不到,不等于操作不会出事。让 Agent 完全自由地登录任意系统,风险从「密码泄露」变成了「越权操作」。加一道轻量审批,成本很低:
```javascript
// approval.js —— 审批台账,10 分钟内有效,一次性
import fs from 'node:fs';
const LEDGER = '/var/lib/agent/approvals.json';
export function isApproved(target, maxAgeMs = 10 * 60 * 1000) {
try {
const rec = JSON.parse(fs.readFileSync(LEDGER, 'utf8'));
return rec.target === target && Date.now() - rec.at < maxAgeMs;
} catch {
return false;
}
}
```
配合一个只有人能执行的小脚本:
```bash
#!/usr/bin/env bash
approve.sh —— 人工确认后写入审批台账
set -euo pipefail
mkdir -p /var/lib/agent
printf '{"target":"%s","at":%s}\n' "$1" "$(($(date +%s) * 1000))" > /var/lib/agent/approvals.json
echo "已批准:$1(10 分钟内有效)"
```
然后在 loginToPortal 开头加上:
```javascript
if (process.env.AGENT_REQUIRE_APPROVAL === '1' && !isApproved(target)) {
return { ok: false, reason: 'awaiting_human_approval' };
}
```
Agent 拿到 awaiting_human_approval 就知道该去找人,而不是反复重试。人在终端跑一次 ./approve.sh staging-portal,Agent 重试即通过。
步骤六:验证「确实没泄露」
写完别急着收工,做一次实证检查:
```bash
1. 确认日志里没有密码
grep -ri "password" ./logs/ 2>/dev/null | grep -v '"password"' || echo "日志干净"
2. 确认进程参数里没有密码
ps -eo args | grep -i "op read" | grep -v grep
3. 确认会话文件权限
ls -l /var/lib/agent/session-*.json
```
第 2 条的输出里如果出现 op read 'op://...',那是正常的——它只有路径没有值。如果出现的是裸密码,说明有人把密码当命令行参数传了,得改成走 op run 或环境注入的方式。
步骤七:凭据轮换,验证 Agent 无需改代码
轮换是这套架构最大的回报。在 1Password App 里把这个条目的密码重新生成一次,然后跑一遍自测:
```bash
node -e "import('./broker.js').then(async m => console.log(await m.loginToPortal('staging-portal')))"
```
如果返回 ok: true,说明轮换完成且 Agent 零改动。如果返回 ok: false,多半是第三方站点那边还没同步新密码,或者账号被锁了。
如果一定要用 CLI 改密码,注意别把明文写进命令:
```bash
密码来自环境变量,但仍然会出现在 ps 里,只在可信单机环境临时用
read -rs NEW_PW
op item edit "staging-portal" --vault "Agent-Login" "password=$NEW_PW"
unset NEW_PW
```
更稳妥的做法是直接在 App 里改,CLI 只用来读取。
轮换完,记得去 1Password 的活动记录里确认这次读取发生在什么时间、由哪个 Service Account 发起,这就是你要的审计闭环。
常见坑与排错
密码进了模型上下文。 九成的情况是让 Agent 直接执行了 op read,stdout 被当成工具结果回灌给模型。检查方法:翻一遍对话记录搜密码前几位。修法是所有凭据读取必须发生在 broker 进程内部,execFileSync 的 stdio 明确设成 ['ignore','pipe','pipe']。
密码进了 shell 历史。 op item get --reveal、op item edit password=xxx 都会留在 ~/.bash_history 里。养成习惯:读取不加 --reveal,修改走 App。
密码进了 ps 输出。 命令行参数对同机其他用户可见。避免把密码作为参数传给任何命令,改用 op run --env-file 或标准输入。
TOTP 报错或登录失败。 一次性验证码有效期通常只有几十秒,不能提前取、不能缓存。每次登录前现取一次;服务器时间不同步也会导致验证码被判无效,检查 NTP。
Playwright 录了 trace 或视频。 排查问题时开的 trace 会记录页面快照,输入框内容可能被一并记录。默认关闭录制,需要时临时开启,用完立刻删。
storageState 文件等同于登录态。 它能直接复用会话,泄露了等于密码泄露。文件权限设 600,放在版本库之外,并在轮换密码后一并清理。
Service Account 权限给大了。 建完必须用 op vault list 和一次故意失败的 op item edit 双向验证,确认只能读、只能读这一个库。
桌面 App 授权模式下 Agent 半夜失效。 App 锁屏或系统休眠会中断会话。要跑无人值守就换 Service Account,并接受「令牌需要定期轮换」这个运维成本。
登录被风控拦。 同一个账号短时间高频登录、出口 IP 频繁变化都会触发风控。固定出口、限制重试次数、失败后指数退避。
下一步建议
跑通最小链路之后,按风险从高到低往下推:
1. 把长期密码换成短期凭据。 如果第三方系统支持 OAuth 客户端凭证、API Key 或设备码流程,优先用它们替代账号密码,Agent 拿到的凭据本身就带过期时间。
2. 给 broker 加网络出口控制。 只允许访问白名单域名,防止 Agent 通过提示注入把会话带到别处。
3. 把 broker 包装成 MCP Server。 如果团队里多个 Agent 都要用,把 login_to_portal 做成标准协议服务,比每个项目抄一份脚本好维护,也方便统一加审计。
4. 把审批接到团队已有的流程里。 现在是在终端跑脚本,可以换成审批机器人或工单系统,把「谁批的、批了什么、什么时候」记进可检索的地方。
5. 定期做一次泄露演练。 主动在日志、对话记录、临时目录里搜一遍密码片段,确认防线还在。
6. 关注 1Password 官方在 Agent / AI 工具方向的新能力。 这块迭代很快,官方文档里关于 Service Account、CLI 和 Agent 集成的部分值得定期回看,具体形态和可用范围以官方页面当前版本为准。
整套东西的价值不在于「Agent 能登录」,而在于Agent 登录这件事被约束在一个可审计、可吊销、可轮换的盒子里。密码不出盒子,出了事关盒子,这就够了。
