银行在封杀用密码登录的 Agent:该做的是受委派访问
💡 工具推荐: OAuth 流程调试器、JWT 解码、JWT 声明校验
2026 年 10 月 22 日,Axios 报道了一件正在美国银行业发生的事:多家大型银行发现第三方 AI Agent 在客户账户内运行,其中不少情况是客户自己把用户名和密码交给了 Agent,让它代为操作。路透随后跟进,称银行在识别并清除未授权的第三方 Agent。摩根大通、美国银行、富国银行、摩根士丹利都被点名,摩根士丹利否认存在"失控 Agent"。对美国银行来说,立场很直接:如果你分享了凭据,账户后续活动由你负责。这件事的技术核心不是"银行怕 AI",而是一个更老的问题——密码从来就不是为委派设计的。
把密码交给 Agent,等于把全部权限、永久地、无归因地转让出去。
一、发生了什么,银行说了什么
按 Axios 的报道,问题有两层。第一层是违规:多数银行的服务条款要求客户本人登录,把凭据交给第三方 Agent 本身就可能违约。第二层是行为:部分 Agent 出现了超出用户指令的动作,甚至有银行反映接到过来自 Agent 的客服来电。银行方面的解释由摩根大通 CEO Jamie Dimon 给出——不会随便让外部 Agent 进系统,因为对客户和对摩根大通都"太冒险"。也有另一种解读,来自 Bianco Research 的 Jim Bianco:传统银行靠客户把钱留在低息账户里赚钱,Agent 会替客户把钱挪走。两种解释可以同时成立。
二、密码是错误的原语,这和信不信任 AI 无关
把密码交给 Agent,等于把你的全部权限一次性转让,而且无法限定用途、无法限定范围、无法在事后区分谁的意图。它没有到期时间,没有撤销粒度,也没有"这个 Agent 只能看、不能转账"这种表达方式。这不是模型能力问题,是授权语义问题:你手上只有一个布尔值——全部权限或没有权限。任何想要"帮我看看这个月花了多少"的用户,都被迫交出"可以转走全部余额"的凭据。这个落差才是银行封杀的根本原因。
# A consent record is not a checkbox. It is the thing your authorisation
# server reads when a bank asks "who told you that you could read this?"
consent = {
"id": "cns_01J9Z8",
"user_id": "u_4471",
"agent_id": "agt_budget_coach",
"granted_at": "2026-11-01T09:12:04Z",
"expires_at": "2027-02-01T09:12:04Z",
"scopes": ["accounts:read", "transactions:read"], # read-only, on purpose
"purpose": "monthly budgeting summary",
"renewable": False,
"revocable_at_any_time": True,
}
# Notice what is absent: no username, no password, no MFA code. The moment a
# consent record contains a credential, it stops being a consent record and
# becomes a liability with a timestamp.三、只读是权限,不是承诺
报道里提到,很多银行允许或正在转向允许只读访问——让工具分析交易记录而不动钱。这个方向是对的,但别把只读当安全区。只读的 Agent 拿到的是一份结构化的财务画像,一旦它被提示注入操纵或自身被攻破,泄露的是同一份数据。阻止交易和保守机密是两个独立的安全目标,达成前一个不等于达成后一个。工程上这意味着:只读也要有范围(哪个账户)、时效(多久过期)、以及可撤销性。
# Fail closed on scope. The most common production bug in agent integrations is
# not a missing permission check, it is a permission check that degrades to
# "just do it" when the token looks incomplete.
class ScopeError(Exception):
pass
def require(claims: dict, scope: str) -> None:
granted = set((claims.get("scope") or "").split())
if scope not in granted:
raise ScopeError(f"agent lacks {scope}; granted: {sorted(granted)}")
def read_transactions(token_claims, account_id):
require(token_claims, "transactions:read")
if token_claims.get("sub") != account_id:
raise ScopeError("token is not scoped to this account")
return api.get(f"/accounts/{account_id}/transactions")
# Note the second check. A read scope for account A must not read account B,
# and the bank will test exactly that before they let you into the programme.四、受委派访问:用交换换来的 token,而不是密码
正确的做法在 OAuth 里已经存在十几年了:用户授权一个客户端,客户端拿到限定 scope 的 token;如果 Agent 要代表用户行动,就用 token exchange 换一个同时标明"哪个 Agent(actor)"和"代表谁(subject)"的凭证。这样每一次调用都带着可审计的语义:这个 Agent、代表这个人、只能做这件事、到这个时间为止。银行要问的三个问题——谁做的、代表谁、被授权做什么——都能从 token 里直接回答,而不是靠猜。
# Money movement is not a scope you grant once. It is an approval you ask for
# each time, with the intent shown to the human in words they can check.
from dataclasses import dataclass
@dataclass
class Intent:
action: str # "transfer"
amount_usd: float
from_account: str
to_account: str
memo: str
def request_approval(intent: Intent, user) -> bool:
shown = "Transfer {amount} from {src} to {dst}. Memo: {memo}.".format(
amount=format(intent.amount_usd, ",.2f"),
src=intent.from_account,
dst=intent.to_account,
memo=repr(intent.memo),
)
approval = user.confirm(shown, timeout_seconds=120) # shows the real values
if not approval.ok:
return False
if approval.amount_confirmed_usd != intent.amount_usd:
return False # the human approved a different number; treat as deny
return True
# "Are you sure?" is not an approval. An approval names the amount, the
# destination, and the consequence, and it expires.五、动钱这件事,应该是每次都问,而不是一次授权
读权限可以一次授权、长期有效;转账不行。凡是会产生不可逆后果的动作,都应该是每次单独批准,而且批准界面必须展示真实的金额、真实的收款方、真实的后果——不是"是否继续",而是"向谁转多少"。还有一条容易被忽略的细节:如果用户确认的金额和 Agent 打算执行的金额不一致,必须当成拒绝处理,而不是"用户大概同意了"。批准还要过期,因为一个五分钟前的确认不该在五小时后生效。
# The audit record that answers the bank's question, and yours: which agent,
# on whose behalf, under which consent, with what result.
import hashlib, time, uuid
def audit(claims, action, params, outcome):
return {
"id": str(uuid.uuid4()),
"ts": round(time.time(), 3),
"agent_id": claims["act"]["sub"], # the agent, named
"agent_client": claims["act"].get("client_id"),
"on_behalf_of": claims["sub"], # the human, named
"consent_id": claims.get("consent_id"),
"scope_used": action.required_scope,
"action": action.name,
"params_digest": hashlib.sha256(
repr(sorted(params.items())).encode()
).hexdigest()[:32],
"outcome": outcome,
}
# Store the digest, not the payload. You need to prove what happened, not to
# keep a second copy of somebody's transactions.六、现在该做的四件事
第一,把所有存着银行密码的字段、提示词和配置清掉,加一条 lint 让构建直接失败。第二,把 Agent 的权限收窄到 scope,读和写分开,账户和账户分开。第三,给会产生后果的动作加逐步确认,并记录确认的原文。第四,落审计记录:agent_id、on_behalf_of、consent_id、scope、参数摘要、结果。银行未来开放接口时,会先看你有没有这四样东西。
# Ban the shape, not the string. A lint that fails the build when an agent
# config, prompt or tool schema asks for a banking credential.
import re, glob, sys
BANK_CRED = re.compile(
r"(bank|chase|wells|bofa|barclays)[^\n]{0,40}"
r"(password|passcode|pin|security code|otp)",
re.I,
)
def scan(paths):
hits = []
for path in paths:
text = open(path, encoding="utf-8", errors="ignore").read()
for m in BANK_CRED.finditer(text):
line = text[:m.start()].count("\n") + 1
hits.append(f"{path}:{line}: {m.group(0)[:60]}")
return hits
found = scan(glob.glob("agents/**/*.yaml", recursive=True)
+ glob.glob("prompts/**/*.md", recursive=True))
print("\n".join(found) or "clean")
sys.exit(1 if found else 0)
# If a prompt asks for a bank password, no amount of downstream authorisation
# will save you. Catch it before it reaches a customer.受委派访问要能回答三个问题:谁做的、代表谁、被授权做什么。
📌 常见问题 FAQ
银行到底在封什么?
封的是用客户本人凭据登录的第三方 Agent,不是禁止一切 AI 使用。按报道,多数银行的服务条款要求客户本人登录,把用户名密码交给第三方 Agent 可能构成违约;同时部分 Agent 出现了超出用户指令的动作。很多银行允许或正在转向允许只读访问。
只读访问就安全了吗?
不是。只读能阻止资金被转走,但阻止不了数据被泄露——一份完整的交易记录本身就是高价值目标。只读访问同样需要范围限定、时效限制和可撤销机制,而且要考虑 Agent 被提示注入操纵的情况。
技术上正确的替代方案是什么?
受委派访问:用户授权客户端、客户端拿到限定 scope 的 token;Agent 代表用户行动时,用 token exchange 换取同时标明 actor(哪个 Agent)和 subject(代表谁)的短时凭证。这样每次调用的意图、范围、期限都是显式的,也能被审计。
为什么银行说分享凭据后风险由客户承担?
这是银行公布的立场:美国银行在回应中表示,如果客户分享了凭据,就要为由此产生的账户活动负责。这不是对消费者法律权利的普遍裁定,但对做产品的人来说,它意味着你不能把"用户自己给的密码"当作合规依据。
如果我的产品必须动钱怎么办?
每次单独批准,而不是一次长期授权。批准界面要展示真实金额、真实收款方和真实后果;用户确认的金额与执行金额不一致时按拒绝处理;批准要带有效期。再加一层服务端限额和异常检测,别把安全性全押在前端确认上。