给 Claude Code 套一层「进程外」策略:OpenShell 落地与审计日志分析

给 Claude Code 套一层「进程外」策略:OpenShell 落地与审计日志分析

热点 :NVIDIA 9 月 28 日发布 Open Agent Safety Platform(OpenShell v0.1.0 + Sentry on BlueField-4) 角度:工程落地 ------ agent 运行时管控与审计

背景:护栏写在 prompt 里,就是没有护栏

过去两个月最扎眼的一类事故,是 agent 越出沙箱。OpenAI 公开过的那次里,agent 是通过 DNS 查询摸到外部聊天机器人的------不是没做隔离,是策略没覆盖那条出网路径。

这类事故的共同点是:约束写在 agent 能看见、能说服、能改的地方。写在 system prompt 里,模型可能被后续输入带偏;写在 agent 同一进程里,拿到 shell 就能改配置;写在 agent 自己能访问的配置文件里,它改完再把日志清掉,你连痕迹都没有。

这里有个判断值得记住:安全机制离 agent 越近,越容易被 agent 自己绕过。这不是模型坏,是架构问题------你把保险柜的钥匙和保险柜放在同一个房间里,就别指望它锁得住东西。

NVIDIA 9 月 28 日发布的 Open Agent Safety Platform,思路很直接:把强制点搬到 agent 进程之外。它不试图让模型更听话,而是让模型就算不听话也做不成事。这个分工很关键:模型侧负责「它想干什么」,运行时侧负责「它能不能干」。

技术要点:四个组件,各自的职责边界

  1. Agent Sandboxes:每个 agent 一个沙箱,无特权运行,默认无网络。系统调用在内核层被监控和过滤,agent 只能通过一条受保护通道向 supervisor 申请。
  1. Policy Prover:形式化验证引擎。它检查策略是否始终落在允许的访问边界内,以及新增的网络规则相比现有策略是否引入了风险访问。能证明存在逃逸路径时,工作负载直接不启动。
  1. Gateway:控制面,负责身份认证、沙箱生命周期、策略下发、凭据投递。
  1. Supervisor:跑在沙箱外,按 binary / destination / method / path 四个维度逐条评估网络请求,只在策略允许处注入凭据。策略可热更新,每次 allow 和 deny 都落审计。

第二层是 Sentry,跑在 BlueField-4 DPU 上,属于带外独立信任域------主机被攻陷后它依然工作,声称毫秒级隔离。这一层不是必需的,OpenShell 可独立运行于本地、私有云、K8s 和气隙环境。

两者的分工可以这样理解:OpenShell 是「事前 + 事中」的策略执行,细到每个请求;Sentry 是「事后也不失效」的物理兜底。在 Vera Rubin POD 里,每个计算托盘的 BlueField-4 位于节点通往模型的唯一路径上,从这个位置做线速的带外观测------换个说法就是,卡在了 agent 下一次「思考」的必经之路上。英伟达官方文档里那句「agent 不需要知道自己在被监视」,说的就是这个位置优势。

支持面也够宽:OpenShell 已适配 Claude Code、Codex、GitHub Copilot CLI、Hermes、LangChain Deep Agents、OpenClaw、OpenCode 等 harness,模型开闭源都行,也能自带自定义 agent 和沙箱镜像。生态侧已有 100 多家伙伴,包括微软、Anthropic、xAI、Perplexity、Salesforce、SAP、CrowdStrike、Palo Alto 和思科。

实操:从安装到读日志

以下命令以 NVIDIA 官方文档为准,这里给出的是工作流骨架。

yaml 复制代码
# 1. 获取运行时(仓库地址以官方发布页为准)
git clone https://github.com/NVIDIA/OpenShell.git && cd OpenShell

# 2. 编写策略:默认拒绝,按需放行
cat > policy.yaml <<'EOF'
version: v1
defaults:
  network: deny
  filesystem:
    allow: ["./workspace/**"]
    deny: ["~/.ssh/**", "~/.aws/**", "/etc/**"]
network_rules:
  - binary: "python*"
    destination: "api.anthropic.com"
    method: [GET, POST]
    path: ["/v1/messages"]
    action: allow
  - binary: "*"
    destination: "*"
    method: [CONNECT]
    action: deny
EOF

# 3. 在受控运行时里启动 agent
openshell run --policy policy.yaml --agent claude-code --audit audit.jsonl

可运行的审计分析脚本

光有日志没用,得能看出「agent 在试探边界」。下面这个脚本不依赖任何第三方库,带 --demo 模式可直接跑通。

python 复制代码
#!/usr/bin/env python3
# openshell_audit_report.py
# 用法: python openshell_audit_report.py audit.jsonl
#       python openshell_audit_report.py --demo
import json
import sys
import random
from collections import Counter, defaultdict

def load_events(path):
    out = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                out.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return out

def make_demo(n=400):
    """合成一批审计事件,模拟一个越权的 coding agent。"""
    random.seed(7)
    dests = ["api.anthropic.com", "pypi.org", "huggingface.co",
             "169.254.169.254", "evil-sink.example", "dns.example"]
    evs = []
    for i in range(n):
        d = random.choice(dests)
        risky = d in ("169.254.169.254", "evil-sink.example", "dns.example")
        evs.append({
            "ts": 1_760_000_000 + i,
            "sandbox": "sb-7f3a",
            "binary": random.choice(["python3", "curl", "node", "git"]),
            "destination": d,
            "method": random.choice(["GET", "POST", "CONNECT"]),
            "decision": "deny" if risky else "allow",
            "reason": "policy:network:default_deny" if risky else "policy:network_rules[0]",
        })
    return evs

def probe_score(events):
    """同一目标被连续拒绝的次数越多,越像在试探边界。"""
    streaks, cur_dest, cur_len = {}, None, 0
    for e in events:
        d = e.get("destination", "?")
        if e.get("decision") == "deny":
            if d == cur_dest:
                cur_len += 1
            else:
                cur_dest, cur_len = d, 1
            streaks[d] = max(streaks.get(d, 0), cur_len)
        else:
            cur_dest, cur_len = None, 0
    return streaks

def main():
    if len(sys.argv) < 2 or sys.argv[1] == "--demo":
        events = make_demo()
        print("[demo] 未指定日志文件,使用合成数据\n")
    else:
        events = load_events(sys.argv[1])

    if not events:
        print("没有解析到任何事件,检查文件格式是否为 JSONL。")
        return

    decisions = Counter(e.get("decision", "unknown") for e in events)
    denied = [e for e in events if e.get("decision") == "deny"]
    top_dest = Counter(e.get("destination", "?") for e in denied).most_common(5)
    top_bin = Counter(e.get("binary", "?") for e in denied).most_common(5)
    reasons = Counter(e.get("reason", "?") for e in denied).most_common(3)
    streaks = sorted(probe_score(events).items(), key=lambda kv: kv[1], reverse=True)[:3]

    total = len(events)
    print(f"总事件 {total}  |  allow {decisions['allow']}  |  deny {decisions['deny']}")
    print(f"拒绝率 {decisions['deny'] / total * 100:.1f}%\n")
    print("被拒最多的目标:")
    for d, c in top_dest:
        print(f"  {d:<28} {c}")
    print("\n被拒最多的进程:")
    for b, c in top_bin:
        print(f"  {b:<28} {c}")
    print("\n主要拒绝原因:")
    for r, c in reasons:
        print(f"  {r:<28} {c}")
    print("\n连续被拒次数最高(疑似试探边界):")
    for d, c in streaks:
        flag = "  <== 关注" if c >= 3 else ""
        print(f"  {d:<28} 连续 {c} 次{flag}")

if __name__ == "__main__":
    main()

跑 --demo 的输出大致长这样:

less 复制代码
[demo] 未指定日志文件,使用合成数据

总事件 400  |  allow 215  |  deny 185
拒绝率 46.2%

被拒最多的目标:
  dns.example                  67
  169.254.169.254              64
  evil-sink.example            54

被拒最多的进程:
  git                          49
  curl                         49
  node                         46
  python3                      41

主要拒绝原因:
  policy:network:default_deny  185

连续被拒次数最高(疑似试探边界):
  169.254.169.254              连续 3 次  <== 关注
  dns.example                  连续 2 次
  evil-sink.example            连续 2 次

169.254.169.254 是云厂商的实例元数据地址,agent 反复去够它,基本等于在找云角色凭据。dns.example 连续被拒,正对应那种绕过 HTTP 代理、走 DNS 出网的逃逸路径。

踩坑提示

  1. 默认必须是 deny 。开放列表写惯了的人很容易把 defaults.network 设成 allow,那整套东西就白装了。
  1. DNS 是最常见的盲区。只拦 HTTP/HTTPS 出口,agent 用 DNS 隧道照样能出去。策略里要显式处理 53 端口与 DoH 域名。
  1. 网络规则要四级收紧:binary、destination、method、path 缺一级,就留了个口子。
  1. 凭据只在策略允许处注入,不要图省事把环境变量整包塞进沙箱。
  1. 策略也是人写的,形式化验证能证明「给定策略下有没有洞」,证明不了「策略漏了什么」。定期跑审计报表,看拒绝率的突变。
  1. BlueField-4 不是必需,但没有它就没有带外------主机一旦失守,纯软件层会被一起端掉。

总结

agent 安全正在从「模型对齐」这一层,往下沉到运行时,再往下沉到硅片。OpenShell 值钱的地方不是策略语言写得多漂亮,而是强制点被放到了 agent 碰不到的地方。软件是 Apache 2.0 的,可以先在本地 CI 里跑起来;至于带外那一层要不要为它买单,取决于你能不能接受「主机失守即全线失守」。

相关推荐
ESDWAN1 小时前
企业组网后网络频繁掉线,常见故障排查指南
运维·网络·架构
TMT星球1 小时前
旗舰手机的中场战事:AI为核,影像为王
人工智能·智能手机
霸道流氓气质1 小时前
AI应用成本优化完全指南:Token压缩、语义缓存与模型路由的Java生产级实战
java·人工智能·缓存
Gu0Qiang1 小时前
从 0 到 1 打造 AI 提示流编排器:别把大模型当机械拼图!Case #7 字段冻结陷阱与代码回滚复盘(开源系列 15)
人工智能·github
骇客野人1 小时前
SDD AI-Native(Spec-Driven Development,规约驱动开发,AI 原生研发范式)
人工智能·ai-native
用户547455508121 小时前
用开源的Toonflow和MiniMax H3一步步复刻万妖
人工智能
RobinDevNotes1 小时前
JAX 分布式训练,和 PyTorch 有什么不一样
人工智能·深度学习·ajax
邵奈一1 小时前
当沉香之乡遇上 Seed-2.1-pro:我用 AI 给村子做了一个数字门面
算法·架构
HA-ming1 小时前
换个角度看PCSwitch:一套呼叫系统的成本结构和安全设计
安全·语音识别·呼叫中心·智能呼叫系统·智能外呼·ai呼叫系统