> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 05-3 NemoClaw 解剖：NVIDIA 為 always-on agent 設計的安全外殼

> NemoClaw 不是另一個 agent 框架，而是把 OpenClaw 或 Hermes 包進 OpenShell 沙箱的硬化外殼。本單元從由來、核心技術、三層堆疊、自訂檔案、四道安全護欄、設計啟發六個面向深度解析，評估「要不要把自家 agent 跑進 NemoClaw」的取捨代價。

export const FrameworkMap = ({lang = "zh", title, layers = []}) => {
  const t = lang === "en" ? {
    hint: "Click a module to see details",
    layer: "Layer",
    module: "Module",
    responsibilities: "Responsibilities",
    path: "Key path",
    relations: "Connected to",
    noSelect: "Select a module above"
  } : {
    hint: "點模組卡片查看職責與關係",
    layer: "層",
    module: "模組",
    responsibilities: "職責",
    path: "關鍵路徑",
    relations: "連結到",
    noSelect: "點上方模組查看詳細說明"
  };
  const [selected, setSelected] = useState(null);
  const safe = Array.isArray(layers) ? layers : [];
  const PALETTE = {
    blue: {
      base: "#5b6c8f",
      bg: "#5b6c8f18",
      border: "#5b6c8f55",
      label: "#46587a"
    },
    purple: {
      base: "#8a6f8e",
      bg: "#8a6f8e18",
      border: "#8a6f8e55",
      label: "#6f5673"
    },
    yellow: {
      base: "#c9a35b",
      bg: "#c9a35b18",
      border: "#c9a35b55",
      label: "#9a7a3c"
    },
    green: {
      base: "#8a9a7b",
      bg: "#8a9a7b18",
      border: "#8a9a7b55",
      label: "#6a7a5c"
    },
    teal: {
      base: "#6f9290",
      bg: "#6f929018",
      border: "#6f929055",
      label: "#527472"
    },
    rose: {
      base: "#ab7269",
      bg: "#ab726918",
      border: "#ab726955",
      label: "#8e564e"
    },
    amber: {
      base: "#b9835c",
      bg: "#b9835c18",
      border: "#b9835c55",
      label: "#96683f"
    }
  };
  const ROTATION = ["blue", "purple", "yellow", "green", "teal", "rose"];
  const getColor = (layer, index) => {
    const key = layer.color && PALETTE[layer.color] ? layer.color : ROTATION[index % ROTATION.length];
    return PALETTE[key];
  };
  const sel = (() => {
    if (!selected) return null;
    for (let li = 0; li < safe.length; li++) {
      const layer = safe[li];
      const m = layer.modules.find(m => m.id === selected);
      if (m) return {
        module: m,
        layerLabel: layer.label,
        color: getColor(layer, li)
      };
    }
    return null;
  })();
  const css = `
.fm-root{--fm-bg:#FAF8F3;--fm-surface2:rgba(0,0,0,0.022);
  --fm-border:rgba(0,0,0,0.08);--fm-text:#2b2722;--fm-dim:#6f6a62;
  --fm-faint:#9a9490;--fm-accent:#bf7551;--fm-card-bg:#fff;
  border:1px solid var(--fm-border);border-radius:14px;background:var(--fm-bg);
  color:var(--fm-text);overflow:hidden;font-family:inherit;}
.dark .fm-root{--fm-bg:#1b1a18;--fm-surface2:rgba(255,255,255,0.025);
  --fm-border:rgba(255,255,255,0.07);--fm-text:#e7e3da;
  --fm-dim:#a8a299;--fm-faint:#6a6560;--fm-card-bg:rgba(255,255,255,0.04);}
.fm-head{padding:13px 18px 11px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--fm-border);flex-wrap:wrap;}
.fm-head-ic{color:var(--fm-accent);flex-shrink:0;}
.fm-head-title{font-size:14px;font-weight:700;letter-spacing:0.01em;flex:1;}
.fm-head-hint{font-size:11.5px;color:var(--fm-faint);}
.fm-layers{padding:14px 16px 10px;display:flex;flex-direction:column;gap:12px;}
.fm-layer{}
.fm-layer-label{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:7px;display:flex;align-items:center;gap:6px;}
.fm-layer-label::after{content:"";flex:1;height:1px;opacity:0.4;}
.fm-modules{display:flex;flex-wrap:wrap;gap:8px;}
.fm-module{display:flex;flex-direction:column;gap:3px;padding:9px 13px;border-radius:9px;
  border:1px solid var(--fm-border);background:var(--fm-card-bg);cursor:pointer;
  transition:border-color .15s,background .15s,box-shadow .15s;text-align:left;
  min-width:120px;max-width:220px;flex:1 1 120px;}
.fm-mod-name{font-size:13px;font-weight:600;line-height:1.3;color:var(--fm-text);}
.fm-mod-summary{font-size:11.5px;line-height:1.5;color:var(--fm-dim);}
.fm-mod-path{font-size:10.5px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-top:2px;word-break:break-all;}
.fm-arrow{display:flex;justify-content:center;padding:2px 0;}
.fm-panel{border-top:1px solid var(--fm-border);padding:14px 18px;background:var(--fm-surface2);min-height:80px;}
.fm-panel-empty{display:flex;align-items:center;justify-content:center;height:60px;
  font-size:12.5px;color:var(--fm-faint);}
.fm-panel-layer{font-size:10.5px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;
  margin-bottom:5px;}
.fm-panel-name{font-size:16px;font-weight:700;margin-bottom:6px;line-height:1.3;}
.fm-panel-path{font-size:11px;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  color:var(--fm-faint);margin-bottom:10px;word-break:break-all;}
.fm-panel-sec{font-size:10.5px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;
  color:var(--fm-dim);margin-bottom:5px;margin-top:10px;}
.fm-panel-detail{font-size:13.5px;line-height:1.65;color:var(--fm-dim);}
.fm-rels{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;}
.fm-rel{font-size:11.5px;padding:3px 9px;border-radius:20px;}
@media (max-width:600px){
  .fm-modules{flex-direction:column;}
  .fm-module{max-width:100%;}
  .fm-head-hint{display:none;}
}
`;
  return <div className="fm-root">
      <style>{css}</style>
      <div className="fm-head">
        <svg className="fm-head-ic" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="2" y="3" width="20" height="5" rx="2" />
          <rect x="2" y="10" width="20" height="5" rx="2" />
          <rect x="2" y="17" width="20" height="5" rx="2" />
        </svg>
        {title && <span className="fm-head-title">{title}</span>}
        <span className="fm-head-hint">{t.hint}</span>
      </div>

      <div className="fm-layers">
        {safe.map((layer, li) => {
    const c = getColor(layer, li);
    return <div className="fm-layer" key={layer.id || li}>
              <div className="fm-layer-label" style={{
      color: c.label
    }}>
                <span>{layer.label}</span>
                <span style={{
      flex: 1,
      height: "1px",
      background: c.border,
      display: "block"
    }} />
              </div>
              <div className="fm-modules">
                {(layer.modules || []).map(mod => {
      const isActive = selected === mod.id;
      return <button key={mod.id} type="button" className="fm-module" style={isActive ? {
        borderColor: c.base,
        background: c.bg,
        boxShadow: "0 0 0 3px " + c.base + "22"
      } : {}} onMouseEnter={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = c.base;
          e.currentTarget.style.background = c.bg;
        }
      }} onMouseLeave={e => {
        if (!isActive) {
          e.currentTarget.style.borderColor = "";
          e.currentTarget.style.background = "";
        }
      }} onClick={() => setSelected(selected === mod.id ? null : mod.id)}>
                      <div className="fm-mod-name" style={isActive ? {
        color: c.label
      } : {}}>{mod.name}</div>
                      {mod.summary && <div className="fm-mod-summary">{mod.summary}</div>}
                      {mod.path && <div className="fm-mod-path">{mod.path}</div>}
                    </button>;
    })}
              </div>
              {li < safe.length - 1 && <div className="fm-arrow">
                  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={getColor(safe[li + 1] || layer, li + 1).base} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{
      opacity: 0.5
    }}>
                    <path d="M12 5v14M5 15l7 7 7-7" />
                  </svg>
                </div>}
            </div>;
  })}
      </div>

      <div className="fm-panel">
        {sel ? <div>
            <div className="fm-panel-layer" style={{
    color: sel.color.label
  }}>{t.layer}: {sel.layerLabel}</div>
            <div className="fm-panel-name">{sel.module.name}</div>
            {sel.module.path && <div className="fm-panel-path">{sel.module.path}</div>}
            {sel.module.detail && <div>
                <div className="fm-panel-sec">{t.responsibilities}</div>
                <div className="fm-panel-detail">{sel.module.detail}</div>
              </div>}
            {sel.module.relations && sel.module.relations.length > 0 && <div>
                <div className="fm-panel-sec">{t.relations}</div>
                <div className="fm-rels">
                  {sel.module.relations.map((r, i) => <span className="fm-rel" key={i} style={{
    background: sel.color.bg,
    color: sel.color.label,
    border: "1px solid " + sel.color.border
  }}>{r.to}{r.label ? " — " + r.label : ""}</span>)}
                </div>
              </div>}
          </div> : <div className="fm-panel-empty">{t.noSelect}</div>}
      </div>
    </div>;
};

export const ArchFlow = ({nodes = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    flow: "Data flow",
    hint: "Click any stage to see what it does",
    stage: "Stage"
  } : {
    flow: "資料流",
    hint: "點任一階段看它做什麼",
    stage: "階段"
  };
  const safe = Array.isArray(nodes) ? nodes : [];
  const [active, setActive] = useState(0);
  if (safe.length === 0) return null;
  const idx = Math.min(active, safe.length - 1);
  const current = safe[idx];
  const css = `
  .af-root{--af-bg:#FAF8F3;--af-surface:rgba(191,117,81,0.06);--af-surface2:rgba(0,0,0,0.025);--af-border:rgba(0,0,0,0.09);--af-text:#2b2722;--af-dim:#6f6a62;--af-faint:#8a8378;--af-rail:rgba(0,0,0,0.12);--af-accent:#bf7551;border:1px solid var(--af-border);border-radius:14px;background:var(--af-bg);color:var(--af-text);overflow:hidden;}
  .dark .af-root{--af-bg:#1b1a18;--af-surface:rgba(207,138,104,0.09);--af-surface2:rgba(255,255,255,0.03);--af-border:rgba(255,255,255,0.08);--af-text:#e7e3da;--af-dim:#a8a299;--af-faint:#8a8378;--af-rail:rgba(255,255,255,0.13);--af-accent:#cf8a68;}
  .af-head{padding:13px 18px 11px;display:flex;align-items:center;gap:9px;flex-wrap:wrap;border-bottom:1px solid var(--af-border);}
  .af-head-ic{color:var(--af-accent);flex-shrink:0;}
  .af-head-t{font-size:12px;font-weight:700;letter-spacing:.6px;color:var(--af-dim);text-transform:uppercase;}
  .af-head-h{font-size:12px;color:var(--af-faint);}
  .af-grid{display:flex;gap:0;align-items:stretch;}
  .af-list{flex:1 1 0;min-width:0;padding:12px 12px 16px;}
  .af-node{display:flex;align-items:stretch;gap:10px;width:100%;text-align:left;background:transparent;border:none;cursor:pointer;padding:0;color:inherit;font:inherit;-webkit-tap-highlight-color:transparent;}
  .af-railcol{width:22px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;}
  .af-dot{width:9px;height:9px;border-radius:50%;margin-top:14px;background:var(--af-faint);opacity:.5;flex-shrink:0;transition:background .15s,box-shadow .15s,opacity .15s;}
  .af-line{width:2px;flex:1 1 0;background:var(--af-rail);min-height:10px;}
  .af-body{flex:1 1 0;min-width:0;padding:9px 12px;margin:2px 0;border-radius:9px;border:1px solid transparent;transition:background .15s,border-color .15s;}
  .af-node:hover .af-body{background:var(--af-surface2);}
  .af-node-on .af-body{background:var(--af-surface);border-color:rgba(191,117,81,.30);}
  .af-node-on .af-dot{background:var(--af-accent);opacity:1;box-shadow:0 0 0 4px rgba(191,117,81,.15);}
  .af-label{font-size:14.5px;font-weight:550;line-height:1.45;}
  .af-node-on .af-label{color:var(--af-accent);}
  .af-detail{width:300px;flex-shrink:0;border-left:1px solid var(--af-border);padding:16px 18px;background:var(--af-surface2);}
  .af-detail-k{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--af-accent);margin-bottom:7px;}
  .af-detail-l{font-size:16px;font-weight:600;margin-bottom:9px;line-height:1.35;}
  .af-detail-d{font-size:13.5px;line-height:1.65;color:var(--af-dim);}
  @media (max-width:640px){.af-grid{flex-direction:column;}.af-detail{width:auto;border-left:none;border-top:1px solid var(--af-border);}}
  `;
  return <div className="af-root">
      <style>{css}</style>
      <div className="af-head">
        <svg className="af-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="8" height="8" x="3" y="3" rx="2" /><path d="M7 11v4a2 2 0 0 0 2 2h4" /><rect width="8" height="8" x="13" y="13" rx="2" /></svg>
        <span className="af-head-t">{t.flow}</span>
        <span className="af-head-h">{t.hint}</span>
      </div>
      <div className="af-grid">
        <div className="af-list">
          {safe.map((n, i) => <button key={i} type="button" className={"af-node" + (i === idx ? " af-node-on" : "")} onClick={() => setActive(i)} onMouseEnter={() => setActive(i)}>
              <div className="af-railcol">
                <div className="af-dot" />
                {i < safe.length - 1 && <div className="af-line" />}
              </div>
              <div className="af-body">
                <div className="af-label">{n.label}</div>
              </div>
            </button>)}
        </div>
        <div className="af-detail">
          <div className="af-detail-k">{t.stage} {idx + 1} / {safe.length}</div>
          <div className="af-detail-l">{current.label}</div>
          <div className="af-detail-d">{current.detail}</div>
        </div>
      </div>
    </div>;
};

export const LearnerPrimer = ({items = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    title: "Before this unit, be honest with yourself",
    sub: "If you can't answer these, that gap is exactly what this unit closes."
  } : {
    title: "讀這個單元前，先誠實面對",
    sub: "這幾題答不出來，正是這個單元要替你補的洞。"
  };
  const css = `
  .lp-root{--lp-bg:#FAF7F1;--lp-surface:rgba(191,117,81,0.05);--lp-border:rgba(0,0,0,0.09);--lp-edge:rgba(191,117,81,0.42);--lp-text:#2b2722;--lp-dim:#6f6a62;--lp-accent:#bf7551;border:1px solid var(--lp-border);border-left:3px solid var(--lp-edge);border-radius:13px;background:var(--lp-bg);color:var(--lp-text);overflow:hidden;margin:1.25rem 0;}
  .dark .lp-root{--lp-bg:#1b1a18;--lp-surface:rgba(207,138,104,0.07);--lp-border:rgba(255,255,255,0.08);--lp-edge:rgba(207,138,104,0.5);--lp-text:#e7e3da;--lp-dim:#a8a299;--lp-accent:#cf8a68;}
  .lp-head{display:flex;align-items:center;gap:9px;padding:13px 18px 11px;border-bottom:1px solid var(--lp-border);background:var(--lp-surface);}
  .lp-ic{color:var(--lp-accent);flex-shrink:0;}
  .lp-htx{display:flex;flex-direction:column;gap:1px;min-width:0;}
  .lp-title{font-size:14px;font-weight:650;line-height:1.3;letter-spacing:.01em;}
  .lp-sub{font-size:12px;color:var(--lp-dim);line-height:1.4;}
  .lp-list{list-style:none;margin:0;padding:10px 18px 14px;display:flex;flex-direction:column;gap:0;}
  .lp-item{display:flex;align-items:baseline;gap:11px;padding:7px 0;font-size:14px;line-height:1.6;border-top:1px solid var(--lp-border);}
  .lp-item:first-child{border-top:none;}
  .lp-mark{flex-shrink:0;color:var(--lp-accent);font-size:13px;font-weight:700;line-height:1.55;font-variant-numeric:tabular-nums;opacity:.85;}
  .lp-q{flex:1 1 0;min-width:0;color:var(--lp-text);}
  @media (max-width:620px){.lp-head{padding:12px 14px 10px;}.lp-list{padding:8px 14px 12px;}}
  `;
  return <div className="lp-root">
      <style>{css}</style>
      <div className="lp-head">
        <svg className="lp-ic" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></svg>
        <span className="lp-htx">
          <span className="lp-title">{t.title}</span>
          <span className="lp-sub">{t.sub}</span>
        </span>
      </div>
      <ul className="lp-list">
        {items.map((q, i) => <li className="lp-item" key={i}>
            <span className="lp-mark">{String(i + 1).padStart(2, "0")}</span>
            <span className="lp-q">{q}</span>
          </li>)}
      </ul>
    </div>;
};

<LearnerPrimer
  lang="zh"
  items={[
"NemoClaw 不是 agent 框架，是硬化外殼。",
"沒有 OpenClaw 或 Hermes 跑在裡面，NemoClaw 沒有意義。",
"always-on agent 沒有四道護欄，提示注入進來就帶著全主機權限跑。",
"安全政策藏在 docker run 指令裡，下個維護者怎麼審？",
"升級上游時硬化層被迫 rebase，那不叫設計，叫負債。",
"把 SSRF 驗證當「要記得開」的選項，已經晚了。",
"agent 邏輯不動只改 blueprint，這才叫關注點分離。",
]}
/>

<Info>
  **這個單元解決什麼問題**

  NemoClaw 是 NVIDIA 在 2026-03 開源的 reference stack，把「總是開著」的 AI 代理（OpenClaw、Hermes）放進 OpenShell 沙箱內運行。它不重新發明 agent 框架，而是提供 CLI 引導、YAML 藍圖、SSRF 驗證、網路政策、managed inference 等強化層，把既有 agent 變成「可被企業環境信任的常駐服務」。本單元從由來、核心技術、系統結構、自訂檔案、安全性、該學什麼六個面向深度解析，並對位到 [05-1](/code-agent/case-studies/openclaw)、[05-2](/code-agent/case-studies/hermes-agent) 與 [05-4](/code-agent/case-studies/three-approaches-comparison)。
</Info>

## 學習目標

* [ ] 能說清 NemoClaw 在堆疊中的位置（不是另一個 agent 框架，是 OpenClaw/Hermes 上層的硬化層）
* [ ] 能描述三層架構：OpenShell 沙箱 → NemoClaw CLI/Blueprint → OpenClaw 或 Hermes 代理
* [ ] 能說出 `blueprint.yaml`、`policies/`、`model-specific-setup/` 三類自訂檔案的職責
* [ ] 能列出 NemoClaw 的四道安全護欄（網路政策、憑證淨化、SSRF 驗證、Docker capability drops）對應的攻擊面
* [ ] 能評估「要不要把自家 agent 跑進 NemoClaw」的取捨代價

***

## 1. 由來與定位

* **發布時點**：2026-03-16，NVIDIA 於 GTC 2026 開源，Apache 2.0 授權（截至 2026-06）
* **為何做**：always-on agent（持續連線、可被訊息喚醒）對企業環境帶來新攻擊面：提示注入誘導 agent 打外網、依賴套件升級夾帶後門、容器逃逸、憑證透過錯誤訊息外洩。NemoClaw 給出「不丟失 agentic 能力但能被 ops 團隊接受」的硬化範本
* **與 [05-1（OpenClaw）](/code-agent/case-studies/openclaw)、[05-2（Hermes-Agent）](/code-agent/case-studies/hermes-agent)的關係**：包覆兩者，不是取代。沒有 agent 跑在裡面，NemoClaw 沒有意義
* **選型理由**：OpenShell（NVIDIA 自家沙箱引擎）+ Claude Code 風格 sub-agent + OpenClaw 預設相容 + 雙 CLI alias 支援 Hermes
* **與 OpenClaw 既有機制相容**：透過 `nemoclaw/` Commander 插件注入命令，OpenClaw 的 heartbeat、SOUL.md、skills 機制不需修改

## 2. 核心技術

* **雙語言堆疊**（依官方 AGENTS.md）：
  * TypeScript：CLI 核心（`src/lib/`）與 OpenClaw plugin（`nemoclaw/src/`）
  * YAML：藍圖（`nemoclaw-blueprint/blueprint.yaml`）與政策（`nemoclaw-blueprint/policies/`）
  * Bash/Python：scripts、E2E 工具
  * CommonJS（`bin/`）：stable entry point launcher，與 TypeScript 編譯產物分開管理版本
* **OpenShell 沙箱**：每個 agent 跑在隔離 runtime 內，工具集與網路存取受限（沙箱細節見 [05-4](/code-agent/case-studies/three-approaches-comparison) 對 sandbox 章節的討論）
* **Managed Inference**：`@aws-sdk/client-bedrock-runtime` + 路由層 + `model-specific-setup` registry 處理「同一 agent 不同模型供應商」的相容性
* **四道安全護欄**：network policies、credential sanitization、SSRF validation、Docker capability drops（細節見第 5 節）
* **多 agent 支援**：原生支援 OpenClaw（預設）與 Hermes（透過 `NEMOCLAW_AGENT=hermes` 環境變數切換，CLI alias `nemohermes`）
* **執行環境需求**：Node.js >= 22.16.0（`engines` 欄宣告，意味著採用較新 V8 與 ESM 特性）

## 3. 系統結構：三層堆疊如何組合

<ArchFlow
  lang="zh"
  nodes={[
{ label: "NemoClaw CLI", detail: "TypeScript 核心（src/lib/）：onboard、credentials、inference、policies、preflight、runner。bin/ 提供 nemoclaw.js 與 nemohermes.js 兩個穩定入口。" },
{ label: "Blueprint（YAML）", detail: "nemoclaw-blueprint/blueprint.yaml 宣告 sandbox 結構、預設政策、推論路由。policies/ 控制 egress，model-specific-setup/ 管理代理-模型相容性 registry。" },
{ label: "OpenShell 沙箱", detail: "每個 agent 跑在獨立隔離的 OpenShell runtime 內，工具集與網路存取由外殼強制限制，沙箱邊界即安全審計單位。" },
{ label: "OpenClaw（預設）", detail: "TypeScript agent 本體，skills/、heartbeat、channels 機制完整保留，透過 nemoclaw/ Commander plugin 接受外殼注入的命令。" },
{ label: "Hermes（可選）", detail: "Python agent，設 NEMOCLAW_AGENT=hermes 切換，CLI alias nemohermes，run_agent.py 為入口。兩者共用同一 NemoClaw package，不需重裝。" },
]}
/>

<FrameworkMap
  lang="zh"
  title="NemoClaw 三層堆疊架構"
  layers={[
{
  id: "hardening",
  color: "rose",
  label: "硬化外殼層 — NemoClaw CLI + Blueprint",
  modules: [
    {
      id: "cli-core",
      name: "CLI 核心",
      path: "src/lib/",
      summary: "onboard、credentials、inference、policies、preflight、runner",
      detail: "TypeScript 核心：引導安裝（onboard）、憑證管理（credentials sanitization）、推論路由（managed inference）、政策套用（policies）、部署前驗證（preflight）與執行控制（runner）。bin/ 提供穩定入口 nemoclaw.js / nemohermes.js。",
      relations: [{ to: "blueprint.yaml", label: "讀取藍圖" }, { to: "OpenShell", label: "控制沙箱" }],
    },
    {
      id: "blueprint",
      name: "blueprint.yaml",
      path: "nemoclaw-blueprint/blueprint.yaml",
      summary: "宣告式藍圖：sandbox 結構、政策、推論路由",
      detail: "整個堆疊的單一事實來源（ops 視角）。宣告 sandbox 結構、預設政策、推論路由。與 openclaw.json（agent 自己的偏好）的差異：blueprint 是「ops 想要的狀態」，進版控、需 preflight 驗證後重啟 sandbox 才生效。",
      relations: [{ to: "policies/", label: "網路政策" }, { to: "model-specific-setup/", label: "模型相容性" }],
    },
    {
      id: "policies",
      name: "policies/",
      path: "nemoclaw-blueprint/policies/",
      summary: "egress 白名單、SSRF 阻擋、連線埠限制",
      detail: "always-on agent 最大攻擊面是「被 prompt injection 誘導打外網」。網路政策是最後一道離開主機的關卡：白名單外網目標、阻擋 SSRF、限制 protocol 與埠號。透過 nemoclaw manage policy 或直接編輯 YAML 客製化。",
      relations: [{ to: "OpenShell", label: "強制執行" }, { to: "SSRF validation", label: "配合" }],
    },
    {
      id: "model-setup",
      name: "model-specific-setup/",
      path: "nemoclaw-blueprint/model-specific-setup/*.json",
      summary: "代理-模型相容性 registry",
      detail: "哪個 agent 配哪個 provider、需要哪些特殊 header 或參數。同樣跑 OpenClaw，切到 Bedrock 與 vLLM 需要不同前綴與 token 限制；這個 registry 把差異收斂到可宣告資料，agent 程式碼不動。",
      relations: [{ to: "blueprint.yaml", label: "由藍圖引用" }],
    },
  ],
},
{
  id: "sandbox",
  color: "amber",
  label: "沙箱層 — OpenShell 隔離執行環境",
  modules: [
    {
      id: "openShell",
      name: "OpenShell",
      path: "runtime（NVIDIA 沙箱引擎）",
      summary: "強制隔離執行環境，沙箱邊界即安全審計單位",
      detail: "每個 agent 跑在獨立隔離的 OpenShell runtime 內，工具集與網路存取由外殼強制限制。同一個 agent 跑在兩個 sandbox 就是兩個審計單位，事件可獨立追溯。Docker capability drops + process limits 防容器逃逸。",
      relations: [{ to: "OpenClaw", label: "容納" }, { to: "Hermes", label: "容納（可選）" }],
    },
    {
      id: "ssrf",
      name: "SSRF validation",
      path: "nemoclaw/src/blueprint/ssrf.ts",
      summary: "阻擋 loopback / link-local / RFC1918 請求",
      detail: "agent 抓 URL 時驗證目標不是內網 metadata 服務（169.254.169.254 等）。拒絕 loopback、link-local、RFC1918 地址，防止 SSRF 類攻擊透過 agent 觸及雲端 metadata 端點。",
      relations: [{ to: "policies/", label: "配合網路政策" }],
    },
  ],
},
{
  id: "agent",
  color: "green",
  label: "代理層 — OpenClaw 或 Hermes（內部 agent 本體）",
  modules: [
    {
      id: "openclaw-agent",
      name: "OpenClaw（預設）",
      path: "agents/openclaw/",
      summary: "TypeScript agent，skills/heartbeat/channels 完整保留",
      detail: "透過 nemoclaw/ Commander plugin 接受外殼注入的命令。OpenClaw 的 heartbeat、SOUL.md、skills 機制不需修改；NemoClaw 是 plugin 而非 fork，升級上游時不必 rebase 硬化層。",
      relations: [{ to: "OpenShell", label: "跑在沙箱內" }, { to: "nemoclaw/ plugin", label: "接收命令" }],
    },
    {
      id: "hermes-agent",
      name: "Hermes（可選）",
      path: "agents/hermes/",
      summary: "設 NEMOCLAW_AGENT=hermes，CLI alias nemohermes",
      detail: "Python agent，設 NEMOCLAW_AGENT=hermes 切換，CLI alias nemohermes。兩者共用同一 NemoClaw package，不需重裝。run_agent.py 為入口。",
      relations: [{ to: "OpenShell", label: "跑在沙箱內" }],
    },
  ],
},
]}
/>

組件對照表（依官方 AGENTS.md）：

| 路徑                                         | 語言               | 職責                                                             |
| ------------------------------------------ | ---------------- | -------------------------------------------------------------- |
| `bin/`                                     | JavaScript (CJS) | CLI launcher（`nemoclaw.js` / `nemohermes.js`）與相容性 helper       |
| `src/lib/`                                 | TypeScript       | 核心 CLI：onboard、credentials、inference、policies、preflight、runner |
| `nemoclaw/`                                | TypeScript       | OpenClaw plugin（Commander 擴充）                                  |
| `nemoclaw/src/blueprint/`                  | TypeScript       | runner、snapshot、SSRF 驗證、state 管理                               |
| `nemoclaw/src/commands/`                   | TypeScript       | slash 命令、migration 狀態                                          |
| `nemoclaw/src/onboard/`                    | TypeScript       | onboarding config                                              |
| `nemoclaw-blueprint/`                      | YAML             | blueprint 定義與網路政策                                              |
| `nemoclaw-blueprint/policies/`             | YAML             | 網路 egress 規則                                                   |
| `nemoclaw-blueprint/model-specific-setup/` | JSON             | 代理-模型相容性 registry                                              |
| `agents/openclaw/`、`agents/hermes/`        | 資產               | 代理部署資產                                                         |
| `.agents/skills/`                          | Markdown         | 11 個 user skill 與 maintainer / contributor 三層                  |

<Tip>
  **看倉庫最快的方式：先讀 AGENTS.md**

  NemoClaw 自己的 `AGENTS.md` 就是給 agent 讀的「這專案怎麼運作」摘要，比 README 更結構化，比源碼更快掌握。讀陌生開源專案時先找它的 `AGENTS.md` / `CLAUDE.md` / `CONTRIBUTING.md` 三件套，是省時間的捷徑。
</Tip>

## 4. 自訂檔案：四類設定面

YAML 為主，TypeScript 為輔，分四層：

### 4.1 Blueprint：整個堆疊的單一事實來源

* `nemoclaw-blueprint/blueprint.yaml`：宣告 sandbox 結構、預設政策、推論路由
* 與「agent 工作區的 `openclaw.json`」的差異：blueprint 是「ops 想要的狀態」，工作區設定是「agent 自己的偏好」；兩者分離避免使用者改壞企業政策
* 改 blueprint 需重啟 sandbox；改工作區設定是 hot-reload

### 4.2 Network Policies：egress 控管

* 位置：`nemoclaw-blueprint/policies/`
* 行為：白名單外網目標、阻擋 SSRF、限制連線埠與 protocol
* 設計理由：always-on agent 最大的攻擊面是「agent 被 prompt injection 誘導去呼叫外網 API 拉資料或上傳 token」；網路政策是最後一道離開主機的關卡
* 客製化：透過 `nemoclaw manage policy` 指令或直接編輯 YAML

### 4.3 Model-Specific Setup：相容性 registry

* 位置：`nemoclaw-blueprint/model-specific-setup/*.json`
* 內容：哪個 agent 配哪個 provider、需要哪些特殊 header 或參數
* 用場：同樣跑 OpenClaw，切到 Bedrock 與切到 vLLM 需要不同 system prompt 前綴與 token 限制；這個 registry 把這層差異收斂到一個檔
* 啟發：把「模型/服務相容性」當作可宣告的資料，不寫死在 agent 程式碼裡

### 4.4 Skills：給 agent 自己讀的文件

* 位置：`.agents/skills/`，三層分眾
  * `nemoclaw-user-*`（10 個，end user：overview、get-started、configure-inference、configure-security、manage-policy、manage-sandboxes、monitor-sandbox、deploy-remote、agent-skills、reference）
  * `nemoclaw-maintainer-*`（project maintainer）
  * `nemoclaw-contributor-*`（codebase contributor）
* 載入機制：用 `nemoclaw-skills-guide` skill 取得完整目錄與決策樹
* 設計觀察：把「文件分眾」當作 first-class metadata，不混在同一層

### 使用者側設定目錄：`~/.nemoclaw/`

上面 4.1 到 4.4 是你（或 ops）在倉庫 `nemoclaw-blueprint/` 裡宣告的硬化設定，進版控、是「這個堆疊該長怎樣」的設計。另有一組不在倉庫、而在你 home 下的檔案：跑完 `nemoclaw onboard` 後，CLI 在你機器上生成的執行期狀態，這才是日常 CLI 直接讀寫的位置（截至 2026-06）：

<Tree>
  <Tree.Folder name="~/.nemoclaw" defaultOpen>
    <Tree.File name="config.json · onboarding 狀態、inference route 設定" />

    <Tree.File name="sandboxes.json · sandbox 登錄表（含 metadata）" />

    <Tree.File name="credentials.json · 使用者 API key（明文 JSON，file mode 600）" />

    <Tree.File name="onboard.json · onboarding 組態快照" />

    <Tree.Folder name="state" defaultOpen>
      <Tree.File name="nemoclaw.json · 執行狀態追蹤" />
    </Tree.Folder>
  </Tree.Folder>
</Tree>

三個要點：

* **`~/.nemoclaw/` 與倉庫 blueprint 的分工**：blueprint（`nemoclaw-blueprint/blueprint.yaml`）是宣告式藍圖、進版控；`~/.nemoclaw/` 是 onboard 在這台機器上落地的執行期狀態、不進版控。前者是設計，後者是這台機器的當前實況。
* **沙箱內另有一份 OpenClaw 設定**：沙箱 container 內部有 `/sandbox/.openclaw/openclaw.json`（agent 設定、模型、gateway）與 `.config-hash`（SHA256 完整性驗證）。這份在容器內、不在 host home，對位的正是 [05-1](/code-agent/case-studies/openclaw) 的 `openclaw.json`，只是被 NemoClaw 包進沙箱代管。
* **uninstall 會整個移除 `~/.nemoclaw/`**：執行期狀態與設計藍圖分離，所以解除安裝清掉 home 狀態，倉庫 blueprint 不受影響。

<Warning>
  **`credentials.json` 是明文、不加密、不進 OS keychain**

  NemoClaw 把使用者輸入的 API key 以明文 JSON 寫進 `~/.nemoclaw/credentials.json`，僅靠 file mode 600 限制權限；官方說明它不加密、也不整合作業系統 keychain。把它排除在 cloud sync（iCloud、OneDrive、Dropbox）與廣域備份之外，否則明文金鑰會被同步上雲。對照之下，provider 的 inference token 由 OpenShell gateway 在 egress 時注入、不落地 host 磁碟，安全模型較好；落地風險集中在這個 `credentials.json`。
</Warning>

## 5. 安全性認知：四道護欄對應的攻擊面

| 護欄                                       | 實作位置                             | 對應攻擊面                                            | 你該驗證的事                                    |
| ---------------------------------------- | -------------------------------- | ------------------------------------------------ | ----------------------------------------- |
| Network policies                         | `nemoclaw-blueprint/policies/`   | agent 被誘導打外網、SSRF、token 上傳                       | 預設 deny 嗎？政策可被 plugin 繞過嗎？                |
| Credential sanitization                  | `src/lib/credentials`            | API key、token 透過日誌或錯誤訊息外洩                        | log 會自動 redact 嗎？哪些 secret 型別被認得？         |
| SSRF validation                          | `nemoclaw/src/blueprint/ssrf.ts` | agent 抓 URL 卻觸及內網 metadata 服務（169.254.169.254 等） | 拒絕 loopback / link-local / RFC1918 嗎？     |
| Docker capability drops + process limits | OpenShell runtime                | 容器逃逸、CPU/記憶體耗盡                                   | 預設 drop ALL caps 嗎？有 no-new-privileges 嗎？ |

<Warning>
  **安全不是附加功能，而是部署前提**

  NemoClaw 的 README 第一個 badge 就是「Security: Report a Vulnerability」，SECURITY.md 規定漏洞須走 NVIDIA PSIRT 或 PGP 信道申報，不開 public issue。**這是給企業環境的訊號：硬化層不是裝飾品，是部署 always-on agent 的前提**。如果你想把 OpenClaw/Hermes 放進企業環境，沒有等效硬化層就不要上 production。
</Warning>

## 6. 該學什麼：六個值得內化的設計

* **外殼即策略**：與其讓 agent 自己決定「能不能打外網」，由外殼宣告（YAML blueprint）並強制執行（OpenShell + policy engine）。把「安全政策」與「agent 邏輯」徹底解耦
* **Managed inference 作為橫切關注點**：模型相容性不是 agent 程式碼的事，是部署層的事。`model-specific-setup` registry 把「同 agent 多模型」變成可宣告資料
* **三層分眾的文件**：user / maintainer / contributor 各一組 skill，避免「一個 README 服務所有人」造成的資訊過載
* **Plugin 而非 fork**：`nemoclaw/` 是 OpenClaw 的 plugin（Commander 擴充），不是 OpenClaw 的 fork。升級上游時不需要 rebase 大改
* **雙 CLI alias**：`nemoclaw`（OpenClaw）與 `nemohermes`（Hermes）共用同一個 package，agent 切換不需重裝
* **Sandbox 邊界即安全審計單位**：OpenShell 既是 runtime，也是審計邊界。同一個 agent 跑在兩個 sandbox 就是兩個審計單位，事件可獨立追溯

<Note>
  **對位到你自己的部署**

  假設你想把團隊內部的 OpenClaw 部署到 staging：

  <Steps>
    <Step title="寫 blueprint">
      `git clone` OpenClaw 後，先寫一份 `nemoclaw-blueprint/blueprint.yaml` 描述「這個 agent 允許打哪幾個內網 API、不能打的外部 host、推論走哪個 provider」。
    </Step>

    <Step title="執行 onboard">
      跑 `npx nemoclaw onboard` 引導，會自動生成 `policies/` 與 `model-specific-setup/`。
    </Step>

    <Step title="部署到 staging">
      `npx nemoclaw deploy-remote` 推到 staging。
    </Step>

    <Step title="監控">
      在 staging 用 `npx nemoclaw monitor-sandbox` 觀察 SSRF 嘗試、policy 違規、credential 曝險。
    </Step>

    <Step title="切換 provider">
      需要切換推論 provider？編輯 `model-specific-setup/` 對應 JSON，重啟 sandbox 即可，agent 程式碼不動。
    </Step>
  </Steps>

  這條路徑展示了「agent 邏輯不動，只改 blueprint」的好處：升級 OpenClaw 上游時不必 rebase 你的硬化層。
</Note>

## 7. 該反問自己什麼

* 你的 always-on agent 有沒有等效的四道護欄？沒有的話風險面是什麼？
* 你的 blueprint 或政策是程式碼（可版控、可審查）還是藏在 `docker run` 指令或 helm values 裡？
* 你的 agent 升級上游時，硬化層會被原樣保留還是必須 rebase？
* 你的 skill 或文件分眾機制是固定的還是隨 agent 演進？
* 你的 SSRF 與 credential sanitization 是「預設開啟」還是「要記得開」？

## 8. 一句生活比喻

NemoClaw 像把一個會走會跑的機器人放進強化玻璃展示間：機器人本身沒換（OpenClaw 還是 OpenClaw、Hermes 還是 Hermes），但展示間決定了它能碰什麼、不能碰什麼、看得到誰、誰看得到它。**展示間的政策（YAML blueprint）是可版控的，機器人的人格（SOUL.md）也是可版控的，兩層分開管理。**

## 9. 與 05-1、05-2、05-4 的對位

| 維度       | 05-1 OpenClaw                 | 05-2 Hermes-Agent      | 05-3 NemoClaw                |
| -------- | ----------------------------- | ---------------------- | ---------------------------- |
| 角色       | agent 本體                      | agent 本體               | agent 的硬化外殼                  |
| 語言       | TypeScript                    | Python                 | TypeScript + YAML + Bash     |
| 沙箱       | 可選（`agents.defaults.sandbox`） | 6 種 terminal 後端        | 強制（OpenShell）                |
| 網路政策     | 工具層級                          | 工具層級                   | blueprint + policy engine    |
| 推論路由     | 客戶端                           | 客戶端                    | managed inference + registry |
| Skill 載入 | `skills/`                     | `skills/`（progressive） | `.agents/skills/` 三層分眾       |
| 企業就緒     | 否（local-first 個人）             | 否（local-first 個人）      | 是（policy-as-code + 漏洞揭露流程）   |

***

## 動手做

<Steps>
  <Step title="讀 AGENTS.md">
    `git clone https://github.com/NVIDIA/NemoClaw` 後跑 `cat AGENTS.md`，對照本單元的「系統結構」段；專案自己為 agent 寫的 AGENTS.md 是最權威的「這專案怎麼運作」摘要。
  </Step>

  <Step title="看 blueprint 形狀">
    閱讀 `nemoclaw-blueprint/blueprint.yaml` 與 `nemoclaw-blueprint/policies/` 一個範例，體會 policy-as-code 的形狀。
  </Step>

  <Step title="觀察三層分眾">
    用 `gh api repos/NVIDIA/NemoClaw/contents/.agents/skills --jq '.[].name'` 列出所有 skill，觀察三層分眾的命名規律。
  </Step>
</Steps>

## 常見誤區

* **以為 NemoClaw 是另一個 agent 框架**：它是硬化外殼，沒有自己的 `AIAgent` 主迴圈。沒有 OpenClaw 或 Hermes 跑在裡面，NemoClaw 沒有意義
* **把 blueprint 當成設定檔隨意改**：blueprint 是企業政策的「期望狀態」，改完需重啟 sandbox 且通過 preflight 驗證；不要繞過 preflight 直接編輯執行中的 sandbox
* **混用 `nemoclaw` 與 `nemohermes` 指令**：兩者管的是不同 agent 的 onboarding；混用會把 OpenClaw 的設定寫到 Hermes 沙箱裡（資料夾結構不同）
* **忽略 `model-specific-setup/` 的版本相容性**：升級模型 provider 後沒更新 registry，agent 會以為新模型有舊模型的相容能力，導致靜默失敗

## 自我檢核

<Check>
  **自我檢核**

  1. 能用一張堆疊圖畫出 OpenShell、NemoClaw、OpenClaw 三者關係嗎？
  2. 能說出四道安全護欄各自對應的攻擊面嗎？
  3. 能解釋為何 `nemoclaw-blueprint/` 採 YAML 而非 TypeScript 或 JSON 嗎？
  4. 能評估「把你的 OpenClaw 部署搬進 NemoClaw」需要改什麼、保留什麼嗎？
</Check>

## 來源與延伸閱讀

事實主張依官方文件，快變動項標註截至 2026-05。

<div className="references">
  * \[1] NVIDIA, "NemoClaw," GitHub repository, Apache 2.0, Mar. 2026. [https://github.com/NVIDIA/NemoClaw](https://github.com/NVIDIA/NemoClaw) （截至 2026-06）

  * \[2] NVIDIA, "NemoClaw Documentation," official docs. [https://docs.nvidia.com/nemoclaw/latest/](https://docs.nvidia.com/nemoclaw/latest/) （截至 2026-06）

  * \[3] NVIDIA, "How It Works," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/about/how-it-works.html](https://docs.nvidia.com/nemoclaw/latest/about/how-it-works.html) （截至 2026-06）

  * \[4] NVIDIA, "Architecture Reference," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/reference/architecture.html](https://docs.nvidia.com/nemoclaw/latest/reference/architecture.html) （截至 2026-06）

  * \[5] NVIDIA, "Ecosystem Positioning," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/about/ecosystem.html](https://docs.nvidia.com/nemoclaw/latest/about/ecosystem.html) （截至 2026-06）

  * \[6] NVIDIA, "Inference Options," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/inference/inference-options.html](https://docs.nvidia.com/nemoclaw/latest/inference/inference-options.html) （截至 2026-06）

  * \[7] NVIDIA, "Network Policies Reference," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/reference/network-policies.html](https://docs.nvidia.com/nemoclaw/latest/reference/network-policies.html) （截至 2026-06）

  * \[8] NVIDIA, "Sandbox Hardening," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/deployment/sandbox-hardening.html](https://docs.nvidia.com/nemoclaw/latest/deployment/sandbox-hardening.html) （截至 2026-06）

  * \[9] NVIDIA, "Security Best Practices," NemoClaw Docs. [https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) （截至 2026-06）

  * \[10] NVIDIA, "SECURITY.md — Vulnerability Disclosure," NemoClaw GitHub. [https://github.com/NVIDIA/NemoClaw/blob/main/SECURITY.md](https://github.com/NVIDIA/NemoClaw/blob/main/SECURITY.md) （截至 2026-06）
</div>

* 銜接：[05-1](/code-agent/case-studies/openclaw) OpenClaw agent 本體、[05-2](/code-agent/case-studies/hermes-agent) Hermes-Agent 本體、[05-4](/code-agent/case-studies/three-approaches-comparison) 三條路線比較（含沙箱深度討論）、[01-6](/code-agent/foundations/harness-engineering) harness 設計六要素（NemoClaw 的安全外殼是其中安全邊界要素的完整實例）。
