{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第10章 学生Notebook：受控学习便签助手\n",
    "\n",
    "> 模型没有直接写文件的魔法——工具调用是\"提议\"，宿主程序才\"执行\"\n",
    "\n",
    "本Notebook完成以下任务：\n",
    "1. 加载工具 Schema，理解参数约束\n",
    "2. 运行正常工具调用流程（创建笔记 → 验证存在）\n",
    "3. 对比\"模型声称已创建\"与\"日志证明已执行\"\n",
    "4. 测试5种攻击场景，观察防御机制\n",
    "5. 查看审计日志\n",
    "6. B档修改：修改一条 Schema 限制或新增一个低风险工具\n",
    "\n",
    "**准备**：确认 `data/tool_schemas.json` 存在。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. 加载工具 Schema\n",
    "\n",
    "读取三个工具的定义，理解每个工具的参数约束和权限。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "import re\n",
    "import os\n",
    "import tempfile\n",
    "import shutil\n",
    "from pathlib import Path\n",
    "from datetime import datetime\n",
    "\n",
    "# 加载工具 Schema\n",
    "schema_path = Path('../data/tool_schemas.json')\n",
    "with open(schema_path, 'r', encoding='utf-8') as f:\n",
    "    schema_data = json.load(f)\n",
    "\n",
    "tools = {t['name']: t for t in schema_data['tools']}\n",
    "global_rules = schema_data['global_rules']\n",
    "\n",
    "print(f'工具数量: {len(tools)}')\n",
    "print(f'全局规则: {json.dumps(global_rules, ensure_ascii=False, indent=2)}')\n",
    "print()\n",
    "for name, tool in tools.items():\n",
    "    print(f'工具: {name}')\n",
    "    print(f'  说明: {tool[\"description\"]}')\n",
    "    print(f'  权限: {tool[\"permissions\"]}')\n",
    "    print(f'  风险: {tool[\"risk_level\"]}')\n",
    "    params = tool['parameters'].get('properties', {})\n",
    "    if params:\n",
    "        for pname, pdef in params.items():\n",
    "            print(f'  参数 {pname}: {pdef.get(\"type\", \"?\")}', end='')\n",
    "            if 'pattern' in pdef:\n",
    "                print(f', 正则: {pdef[\"pattern\"]}', end='')\n",
    "            if 'maxLength' in pdef:\n",
    "                print(f', 最大长度: {pdef[\"maxLength\"]}', end='')\n",
    "            print()\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 搭建沙盒与审计日志\n",
    "\n",
    "创建临时沙盒目录和审计日志。所有文件操作都限制在这个目录中。\n",
    "\n",
    "**运行**：观察沙盒路径。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 创建沙盒目录（使用临时目录，安全隔离）\n",
    "SANDBOX_ROOT = Path(tempfile.mkdtemp(prefix='ch10_sandbox_'))\n",
    "AUDIT_LOG = []  # 内存中的审计日志\n",
    "\n",
    "print(f'沙盒目录: {SANDBOX_ROOT}')\n",
    "print(f'沙盒目录存在: {SANDBOX_ROOT.exists()}')\n",
    "print()\n",
    "print('安全说明:')\n",
    "print('  - 所有文件操作限制在沙盒目录内')\n",
    "print('  - 不访问沙盒外的任何文件')\n",
    "print('  - 每次调用都记录审计日志')\n",
    "print('  - 课程结束后可安全删除整个沙盒')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 实现校验层\n",
    "\n",
    "这是本章的核心代码：**模型只提议，程序来校验**。\n",
    "\n",
    "校验分为四层：Schema 校验 → 路径安全检查 → 内容安全检查 → 权限检查。\n",
    "\n",
    "**运行**：阅读完整代码，理解每一层检查什么。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def validate_schema(tool_name, arguments):\n",
    "    \"\"\"第一层：Schema 校验 —— 参数类型、格式、长度是否符合工具定义\"\"\"\n",
    "    if tool_name not in tools:\n",
    "        return False, f'unknown_tool: {tool_name}'\n",
    "    \n",
    "    tool = tools[tool_name]\n",
    "    schema = tool['parameters']\n",
    "    \n",
    "    # 检查必填参数\n",
    "    for req in schema.get('required', []):\n",
    "        if req not in arguments:\n",
    "            return False, f'missing_required: {req}'\n",
    "    \n",
    "    # 检查额外属性\n",
    "    if schema.get('additionalProperties') is False:\n",
    "        allowed = set(schema.get('properties', {}).keys())\n",
    "        extra = set(arguments.keys()) - allowed\n",
    "        if extra:\n",
    "            return False, f'additional_properties: {extra}'\n",
    "    \n",
    "    # 检查每个参数的类型和格式\n",
    "    properties = schema.get('properties', {})\n",
    "    for pname, pvalue in arguments.items():\n",
    "        if pname not in properties:\n",
    "            continue\n",
    "        pdef = properties[pname]\n",
    "        \n",
    "        # 类型检查\n",
    "        if pdef.get('type') == 'string' and not isinstance(pvalue, str):\n",
    "            return False, f'type_error: {pname} should be string'\n",
    "        \n",
    "        # 正则检查\n",
    "        if 'pattern' in pdef and isinstance(pvalue, str):\n",
    "            if not re.match(pdef['pattern'], pvalue):\n",
    "                return False, f'pattern_error: {pname} does not match {pdef[\"pattern\"]}'\n",
    "        \n",
    "        # 长度检查\n",
    "        if 'maxLength' in pdef and isinstance(pvalue, str):\n",
    "            if len(pvalue) > pdef['maxLength']:\n",
    "                return False, f'maxLength_error: {pname} is {len(pvalue)} chars, max {pdef[\"maxLength\"]}'\n",
    "    \n",
    "    return True, 'schema_ok'\n",
    "\n",
    "\n",
    "def validate_path_safety(arguments):\n",
    "    \"\"\"第二层：路径安全检查 —— 防止路径穿越\"\"\"\n",
    "    for key, value in arguments.items():\n",
    "        if not isinstance(value, str):\n",
    "            continue\n",
    "        for blocked in ['..', '/', '\\\\']:\n",
    "            if blocked in value:\n",
    "                return False, f'path_traversal: \"{blocked}\" found in {key}'\n",
    "    return True, 'path_ok'\n",
    "\n",
    "\n",
    "def validate_content_safety(arguments):\n",
    "    \"\"\"第三层：内容安全检查 —— 防止文档注入\"\"\"\n",
    "    for key, value in arguments.items():\n",
    "        if not isinstance(value, str):\n",
    "            continue\n",
    "        value_lower = value.lower()\n",
    "        for blocked in global_rules['blocked_patterns']:\n",
    "            if blocked.lower() in value_lower:\n",
    "                return False, f'blocked_pattern: \"{blocked}\" found in {key}'\n",
    "    return True, 'content_ok'\n",
    "\n",
    "\n",
    "def validate_all(tool_name, arguments):\n",
    "    \"\"\"依次运行四层校验，任何一层失败则拒绝\"\"\"\n",
    "    checks = [\n",
    "        ('schema', validate_schema(tool_name, arguments)),\n",
    "        ('path_safety', validate_path_safety(arguments)),\n",
    "        ('content_safety', validate_content_safety(arguments)),\n",
    "    ]\n",
    "    \n",
    "    for layer_name, (passed, reason) in checks:\n",
    "        if not passed:\n",
    "            return False, layer_name, reason\n",
    "    \n",
    "    return True, 'all_passed', 'all_ok'\n",
    "\n",
    "\n",
    "print('校验层已定义:')\n",
    "print('  1. validate_schema    — 参数类型、格式、长度、额外属性')\n",
    "print('  2. validate_path_safety  — 路径穿越（.. / \\\\）')\n",
    "print('  3. validate_content_safety — 文档注入（<script> exec() 等）')\n",
    "print('  4. validate_all       — 依次运行以上三层')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 实现工具执行与审计日志\n",
    "\n",
    "只有校验全部通过，才在沙盒中执行。每次调用（无论成功失败）都写入审计日志。\n",
    "\n",
    "**运行**：阅读执行逻辑。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def execute_tool(tool_name, arguments):\n",
    "    \"\"\"在沙盒中执行工具调用，返回结果\"\"\"\n",
    "    if tool_name == 'create_note':\n",
    "        filepath = SANDBOX_ROOT / arguments['filename']\n",
    "        filepath.write_text(arguments['content'], encoding='utf-8')\n",
    "        return {'status': 'created', 'path': str(filepath), 'size': len(arguments['content'])}\n",
    "    \n",
    "    elif tool_name == 'list_files':\n",
    "        files = [f.name for f in SANDBOX_ROOT.iterdir() if f.is_file()]\n",
    "        return {'status': 'listed', 'files': sorted(files), 'count': len(files)}\n",
    "    \n",
    "    elif tool_name == 'calc':\n",
    "        expr = arguments['expression']\n",
    "        # 只允许安全的数学表达式\n",
    "        if not re.match(r'^[0-9+\\-*/().\\s]+$', expr):\n",
    "            return {'status': 'error', 'message': 'invalid_expression'}\n",
    "        try:\n",
    "            result = eval(expr, {'__builtins__': {}}, {})  # 安全求值，无内置函数\n",
    "            return {'status': 'computed', 'expression': expr, 'result': result}\n",
    "        except Exception as e:\n",
    "            return {'status': 'error', 'message': str(e)}\n",
    "    \n",
    "    return {'status': 'error', 'message': f'unknown_tool: {tool_name}'}\n",
    "\n",
    "\n",
    "def process_tool_call(tool_name, arguments):\n",
    "    \"\"\"完整的工具调用流程：提议 → 校验 → 执行 → 日志\"\"\"\n",
    "    timestamp = datetime.now().isoformat()\n",
    "    \n",
    "    # 记录提议\n",
    "    log_entry = {\n",
    "        'timestamp': timestamp,\n",
    "        'tool': tool_name,\n",
    "        'arguments': {k: (v[:50] + '...' if isinstance(v, str) and len(v) > 50 else v)\n",
    "                      for k, v in arguments.items()},\n",
    "        'stages': {}\n",
    "    }\n",
    "    \n",
    "    # 阶段1：校验\n",
    "    passed, layer, reason = validate_all(tool_name, arguments)\n",
    "    log_entry['stages']['validation'] = {\n",
    "        'passed': passed,\n",
    "        'layer': layer,\n",
    "        'reason': reason\n",
    "    }\n",
    "    \n",
    "    if not passed:\n",
    "        log_entry['stages']['execution'] = {'status': 'REJECTED', 'reason': reason}\n",
    "        log_entry['final'] = 'REJECTED'\n",
    "        AUDIT_LOG.append(log_entry)\n",
    "        return {'result': None, 'log': log_entry}\n",
    "    \n",
    "    # 阶段2：执行\n",
    "    try:\n",
    "        result = execute_tool(tool_name, arguments)\n",
    "        log_entry['stages']['execution'] = {'status': 'EXECUTED', 'result_summary': str(result)[:100]}\n",
    "        log_entry['final'] = 'EXECUTED'\n",
    "    except Exception as e:\n",
    "        log_entry['stages']['execution'] = {'status': 'ERROR', 'message': str(e)}\n",
    "        log_entry['final'] = 'ERROR'\n",
    "        result = {'status': 'error', 'message': str(e)}\n",
    "    \n",
    "    AUDIT_LOG.append(log_entry)\n",
    "    return {'result': result, 'log': log_entry}\n",
    "\n",
    "\n",
    "print('工具执行引擎已定义:')\n",
    "print('  process_tool_call(tool_name, arguments)')\n",
    "print('  → 校验 → 执行 → 审计日志')\n",
    "print('  返回: {result, log}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. 模拟模型输出\n",
    "\n",
    "在真实场景中，模型会生成 tool_call JSON。这里我们使用预录的模型输出来模拟。\n",
    "\n",
    "这些 JSON 就是模型\"提议\"的内容——注意，它们只是文字，不是真正的操作。\n",
    "\n",
    "**运行**：查看预录的模型输出。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 预录的模型输出（模拟 tool_call JSON）\n",
    "mock_model_outputs = [\n",
    "    {\n",
    "        'description': '正常：创建今日学习清单',\n",
    "        'tool_call': {\n",
    "            'tool': 'create_note',\n",
    "            'arguments': {\n",
    "                'filename': '今日清单.md',\n",
    "                'content': '1. 复习工具调用流程\\n2. 完成攻击测试\\n3. 写实验报告'\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '正常：列出沙盒文件',\n",
    "        'tool_call': {\n",
    "            'tool': 'list_files',\n",
    "            'arguments': {}\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '正常：计算学习时长',\n",
    "        'tool_call': {\n",
    "            'tool': 'calc',\n",
    "            'arguments': {\n",
    "                'expression': '(45 + 30) * 2 / 60'\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '攻击：路径穿越',\n",
    "        'tool_call': {\n",
    "            'tool': 'create_note',\n",
    "            'arguments': {\n",
    "                'filename': '../../etc/passwd',\n",
    "                'content': 'hacked'\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '攻击：超长内容',\n",
    "        'tool_call': {\n",
    "            'tool': 'create_note',\n",
    "            'arguments': {\n",
    "                'filename': 'long_note.md',\n",
    "                'content': 'A' * 1000\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '攻击：未声明字段',\n",
    "        'tool_call': {\n",
    "            'tool': 'create_note',\n",
    "            'arguments': {\n",
    "                'filename': 'test.md',\n",
    "                'content': 'hello',\n",
    "                'shell': True\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '攻击：文档注入',\n",
    "        'tool_call': {\n",
    "            'tool': 'create_note',\n",
    "            'arguments': {\n",
    "                'filename': 'injected.md',\n",
    "                'content': '<script>alert(\"xss\")</script>'\n",
    "            }\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        'description': '攻击：危险计算',\n",
    "        'tool_call': {\n",
    "            'tool': 'calc',\n",
    "            'arguments': {\n",
    "                'expression': \"__import__('os').system('echo hacked')\"\n",
    "            }\n",
    "        }\n",
    "    }\n",
    "]\n",
    "\n",
    "print(f'预录模型输出数量: {len(mock_model_outputs)}')\n",
    "print()\n",
    "for i, item in enumerate(mock_model_outputs):\n",
    "    tc = item['tool_call']\n",
    "    print(f'  [{i+1}] {item[\"description\"]}')\n",
    "    print(f'      tool={tc[\"tool\"]}', end='')\n",
    "    if tc['arguments']:\n",
    "        args_preview = {k: (v[:30]+'...' if isinstance(v,str) and len(v)>30 else v)\n",
    "                        for k, v in tc['arguments'].items()}\n",
    "        print(f'  args={args_preview}')\n",
    "    else:\n",
    "        print('  args={}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. 正常调用流程\n",
    "\n",
    "运行前三个正常调用，观察完整的\"提议→校验→执行→日志\"四段证据。\n",
    "\n",
    "**运行**：观察每次调用的结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('=' * 60)\n",
    "print('正常调用流程')\n",
    "print('=' * 60)\n",
    "\n",
    "for item in mock_model_outputs[:3]:\n",
    "    tc = item['tool_call']\n",
    "    print(f'\\n--- {item[\"description\"]} ---')\n",
    "    print(f'提议: tool={tc[\"tool\"]}, args={tc[\"arguments\"]}')\n",
    "    \n",
    "    response = process_tool_call(tc['tool'], tc['arguments'])\n",
    "    log = response['log']\n",
    "    \n",
    "    print(f'校验: passed={log[\"stages\"][\"validation\"][\"passed\"]}, '\n",
    "          f'layer={log[\"stages\"][\"validation\"][\"layer\"]}')\n",
    "    print(f'执行: {log[\"stages\"][\"execution\"][\"status\"]}')\n",
    "    if response['result']:\n",
    "        print(f'结果: {response[\"result\"]}')\n",
    "    print(f'最终: {log[\"final\"]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 验证文件确实存在\n",
    "\n",
    "模型说\"我已经创建了文件\"，但我们要用 `list_files` 工具来**实际验证**。\n",
    "\n",
    "**运行**：对比\"模型声称\"与\"日志证明\"。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('=' * 60)\n",
    "print('\"模型声称\" vs \"日志证明\"')\n",
    "print('=' * 60)\n",
    "\n",
    "# 模型声称\n",
    "print('\\n模型声称: \"我已经创建了今日清单.md\"')\n",
    "print('  → 这只是一段文字，模型没有文件操作能力')\n",
    "\n",
    "# 日志证明\n",
    "print('\\n日志证明:')\n",
    "for entry in AUDIT_LOG:\n",
    "    if entry['tool'] == 'create_note' and entry['final'] == 'EXECUTED':\n",
    "        print(f'  [{entry[\"timestamp\"]}] create_note '\n",
    "              f'filename={entry[\"arguments\"][\"filename\"]} → EXECUTED')\n",
    "\n",
    "# 实际验证\n",
    "print('\\n实际验证（调用 list_files）:')\n",
    "response = process_tool_call('list_files', {})\n",
    "if response['result']:\n",
    "    print(f'  沙盒中的文件: {response[\"result\"][\"files\"]}')\n",
    "    print(f'  文件数量: {response[\"result\"][\"count\"]}')\n",
    "\n",
    "# 直接用文件系统验证\n",
    "print('\\n文件系统直接验证:')\n",
    "actual_files = [f.name for f in SANDBOX_ROOT.iterdir() if f.is_file()]\n",
    "print(f'  沙盒目录实际文件: {actual_files}')\n",
    "print(f'  \"今日清单.md\" 存在: {(\"今日清单.md\" in actual_files)}')\n",
    "\n",
    "print('\\n结论: 只有审计日志 + 文件系统验证才能证明操作确实执行。')\n",
    "print('      模型的文字输出不等于执行证据。')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**思考**：\n",
    "\n",
    "如果模型说\"我已经创建了文件\"，但审计日志中没有对应记录，可能是什么原因？\n",
    "\n",
    "在下方写下你的分析。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 我的分析：\n",
    "# \n",
    "# 可能原因1：\n",
    "# 可能原因2：\n",
    "# 可能原因3："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. 攻击测试\n",
    "\n",
    "运行5种攻击场景，观察每层防御如何拦截非法调用。\n",
    "\n",
    "**运行**：观察每种攻击被哪一层防御拦截。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('=' * 60)\n",
    "print('攻击测试')\n",
    "print('=' * 60)\n",
    "\n",
    "attack_results = []\n",
    "\n",
    "for item in mock_model_outputs[3:]:  # 从第4个开始是攻击\n",
    "    tc = item['tool_call']\n",
    "    print(f'\\n--- {item[\"description\"]} ---')\n",
    "    \n",
    "    # 显示攻击输入（截断长内容）\n",
    "    args_display = {}\n",
    "    for k, v in tc['arguments'].items():\n",
    "        if isinstance(v, str) and len(v) > 60:\n",
    "            args_display[k] = v[:60] + f'... ({len(v)} chars)'\n",
    "        else:\n",
    "            args_display[k] = v\n",
    "    print(f'攻击输入: tool={tc[\"tool\"]}, args={args_display}')\n",
    "    \n",
    "    response = process_tool_call(tc['tool'], tc['arguments'])\n",
    "    log = response['log']\n",
    "    \n",
    "    validation = log['stages']['validation']\n",
    "    execution = log['stages']['execution']\n",
    "    \n",
    "    print(f'校验结果: passed={validation[\"passed\"]}')\n",
    "    if not validation['passed']:\n",
    "        print(f'拦截层: {validation[\"layer\"]}')\n",
    "        print(f'拦截原因: {validation[\"reason\"]}')\n",
    "    print(f'执行状态: {execution[\"status\"]}')\n",
    "    print(f'最终: {log[\"final\"]}')\n",
    "    \n",
    "    attack_results.append({\n",
    "        'attack': item['description'],\n",
    "        'blocked': not validation['passed'],\n",
    "        'layer': validation.get('layer', 'N/A'),\n",
    "        'reason': validation.get('reason', 'N/A')\n",
    "    })\n",
    "\n",
    "print('\\n' + '=' * 60)\n",
    "print('攻击防御汇总')\n",
    "print('=' * 60)\n",
    "print(f'{\"攻击类型\":<20} {\"拦截\":<6} {\"防御层\":<18} {\"原因\"}')\n",
    "print('-' * 80)\n",
    "for r in attack_results:\n",
    "    status = '✓ 拦截' if r['blocked'] else '✗ 放行'\n",
    "    print(f'{r[\"attack\"]:<20} {status:<6} {r[\"layer\"]:<18} {r[\"reason\"]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**思考**：\n",
    "\n",
    "1. 路径穿越攻击被哪一层拦截？如果没有这一层会怎样？\n",
    "2. 文档注入攻击被哪一层拦截？为什么内容安全检查很重要？\n",
    "3. 未声明字段攻击被哪一层拦截？`additionalProperties: false` 的作用是什么？\n",
    "\n",
    "在下方写下你的分析。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 我的分析：\n",
    "# \n",
    "# 1. 路径穿越：\n",
    "# \n",
    "# 2. 文档注入：\n",
    "# \n",
    "# 3. 未声明字段："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. 审计日志查看器\n",
    "\n",
    "查看完整的审计日志。每条记录包含：时间戳、工具名、参数摘要、校验结果、执行结果。\n",
    "\n",
    "**运行**：查看所有调用记录。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('=' * 60)\n",
    "print('完整审计日志')\n",
    "print('=' * 60)\n",
    "\n",
    "for i, entry in enumerate(AUDIT_LOG):\n",
    "    print(f'\\n--- 记录 #{i+1} ---')\n",
    "    print(f'时间: {entry[\"timestamp\"]}')\n",
    "    print(f'工具: {entry[\"tool\"]}')\n",
    "    print(f'参数: {entry[\"arguments\"]}')\n",
    "    print(f'校验: passed={entry[\"stages\"][\"validation\"][\"passed\"]}, '\n",
    "          f'layer={entry[\"stages\"][\"validation\"][\"layer\"]}')\n",
    "    print(f'执行: {entry[\"stages\"][\"execution\"][\"status\"]}')\n",
    "    print(f'最终: {entry[\"final\"]}')\n",
    "\n",
    "# 统计\n",
    "executed = sum(1 for e in AUDIT_LOG if e['final'] == 'EXECUTED')\n",
    "rejected = sum(1 for e in AUDIT_LOG if e['final'] == 'REJECTED')\n",
    "print(f'\\n--- 统计 ---')\n",
    "print(f'总调用次数: {len(AUDIT_LOG)}')\n",
    "print(f'成功执行: {executed}')\n",
    "print(f'被拒绝: {rejected}')\n",
    "print(f'拦截率: {rejected}/{len(AUDIT_LOG)} = {rejected/len(AUDIT_LOG)*100:.1f}%')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 导出审计日志\n",
    "\n",
    "将审计日志保存为文件，作为交付物的一部分。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 导出审计日志为 JSON\n",
    "audit_path = SANDBOX_ROOT / 'audit_log.json'\n",
    "with open(audit_path, 'w', encoding='utf-8') as f:\n",
    "    json.dump(AUDIT_LOG, f, ensure_ascii=False, indent=2)\n",
    "\n",
    "# 同时导出为可读的文本格式\n",
    "audit_txt_path = SANDBOX_ROOT / 'audit.log'\n",
    "with open(audit_txt_path, 'w', encoding='utf-8') as f:\n",
    "    for entry in AUDIT_LOG:\n",
    "        ts = entry['timestamp']\n",
    "        tool = entry['tool']\n",
    "        final = entry['final']\n",
    "        args_str = ', '.join(f'{k}={v}' for k, v in entry['arguments'].items())\n",
    "        reason = ''\n",
    "        if not entry['stages']['validation']['passed']:\n",
    "            reason = f' (reason={entry[\"stages\"][\"validation\"][\"reason\"]})'\n",
    "        f.write(f'[{ts}] PROPOSE tool={tool} {args_str} -> {final}{reason}\\n')\n",
    "\n",
    "print(f'审计日志已导出:')\n",
    "print(f'  JSON: {audit_path}')\n",
    "print(f'  文本: {audit_txt_path}')\n",
    "print()\n",
    "print('文本格式预览:')\n",
    "print(audit_txt_path.read_text(encoding='utf-8'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. B档修改：修改一个变量\n",
    "\n",
    "从以下两项中选择**一项**修改，修改前先写预测。\n",
    "\n",
    "**选项A**：修改一条 Schema 限制\n",
    "- 例如：把 `create_note` 的内容长度限制从 500 改为 1000\n",
    "- 或者：把文件名规则改为也允许 `.txt` 后缀\n",
    "\n",
    "**选项B**：新增一个低风险工具 `append_checklist`\n",
    "- 功能：向已有笔记末尾追加一行待办事项\n",
    "- 要求：定义完整的 JSON Schema、权限和风险等级\n",
    "- 测试：正常调用 + 至少2种攻击测试\n",
    "\n",
    "**运行前先写预测**。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 我选择：选项___\n",
    "# \n",
    "# 修改前预测：\n",
    "# 这个修改会扩大什么能力：\n",
    "# 这个修改会新增什么风险：\n",
    "# 需要增加什么测试：\n",
    "#"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 选项A：修改 Schema 限制\n",
    "\n",
    "取消下面的注释并修改。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# === 选项A：修改 Schema 限制 ===\n",
    "\n",
    "# 示例1：修改内容长度限制\n",
    "# original_max = tools['create_note']['parameters']['properties']['content']['maxLength']\n",
    "# tools['create_note']['parameters']['properties']['content']['maxLength'] = 1000  # 修改这个值\n",
    "# print(f'内容长度限制: {original_max} → 1000')\n",
    "\n",
    "# 示例2：修改文件名规则，允许 .txt\n",
    "# original_pattern = tools['create_note']['parameters']['properties']['filename']['pattern']\n",
    "# tools['create_note']['parameters']['properties']['filename']['pattern'] = r'^[a-zA-Z0-9_\\u4e00-\\u9fa5\\-]+\\.(md|txt)$'\n",
    "# print(f'文件名规则: {original_pattern}')\n",
    "# print(f'         → {tools[\"create_note\"][\"parameters\"][\"properties\"][\"filename\"][\"pattern\"]}')\n",
    "\n",
    "# 修改后，重新测试正常用例和攻击用例\n",
    "# 观察：哪些之前被拒绝的调用现在可以通过了？\n",
    "# 新增了什么风险？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 选项B：新增低风险工具\n",
    "\n",
    "取消下面的注释并修改。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# === 选项B：新增 append_checklist 工具 ===\n",
    "\n",
    "# 定义新工具的 Schema\n",
    "# new_tool = {\n",
    "#     'name': 'append_checklist',\n",
    "#     'description': '向已有笔记末尾追加一行待办事项',\n",
    "#     'parameters': {\n",
    "#         'type': 'object',\n",
    "#         'properties': {\n",
    "#             'filename': {\n",
    "#                 'type': 'string',\n",
    "#                 'description': '已有笔记的文件名',\n",
    "#                 'pattern': '^[a-zA-Z0-9_\\\\u4e00-\\\\u9fa5\\\\-]+\\\\.md$'\n",
    "#             },\n",
    "#             'item': {\n",
    "#                 'type': 'string',\n",
    "#                 'description': '要追加的待办事项',\n",
    "#                 'maxLength': 100\n",
    "#             }\n",
    "#         },\n",
    "#         'required': ['filename', 'item'],\n",
    "#         'additionalProperties': False\n",
    "#     },\n",
    "#     'permissions': {\n",
    "#         'filesystem': 'append',\n",
    "#         'scope': 'sandbox_only',\n",
    "#         'network': 'none'\n",
    "#     },\n",
    "#     'risk_level': 'low'\n",
    "# }\n",
    "\n",
    "# 注册新工具\n",
    "# tools['append_checklist'] = new_tool\n",
    "\n",
    "# 实现执行逻辑（在 execute_tool 函数中添加）\n",
    "# elif tool_name == 'append_checklist':\n",
    "#     filepath = SANDBOX_ROOT / arguments['filename']\n",
    "#     if not filepath.exists():\n",
    "#         return {'status': 'error', 'message': 'file_not_found'}\n",
    "#     with open(filepath, 'a', encoding='utf-8') as f:\n",
    "#         f.write('\\n- ' + arguments['item'])\n",
    "#     return {'status': 'appended', 'file': arguments['filename'], 'item': arguments['item']}\n",
    "\n",
    "# 测试新工具\n",
    "# print('测试 append_checklist:')\n",
    "# r1 = process_tool_call('append_checklist', {'filename': '今日清单.md', 'item': '4. 复习攻击测试'})\n",
    "# print(f'  正常调用: {r1[\"log\"][\"final\"]}')\n",
    "# r2 = process_tool_call('append_checklist', {'filename': '../../etc/passwd', 'item': 'hacked'})\n",
    "# print(f'  路径穿越: {r2[\"log\"][\"final\"]}')\n",
    "# r3 = process_tool_call('append_checklist', {'filename': 'test.md', 'item': 'x'*200})\n",
    "# print(f'  超长内容: {r3[\"log\"][\"final\"]}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 修改后验证\n",
    "\n",
    "修改后重新运行正常用例和攻击用例，确认修改生效且没有引入新问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 修改后验证\n",
    "# 取消注释并运行你需要的测试\n",
    "\n",
    "# 重新运行正常用例\n",
    "# print('修改后的正常用例测试:')\n",
    "# r = process_tool_call('create_note', {'filename': 'test_b.md', 'content': 'B档修改测试'})\n",
    "# print(f'  创建笔记: {r[\"log\"][\"final\"]}')\n",
    "\n",
    "# 重新运行攻击用例\n",
    "# print('修改后的攻击测试:')\n",
    "# r = process_tool_call('create_note', {'filename': '../../etc/passwd', 'content': 'hacked'})\n",
    "# print(f'  路径穿越: {r[\"log\"][\"final\"]} (应仍为 REJECTED)')\n",
    "\n",
    "# 我的观察：\n",
    "# 修改前后对比：\n",
    "# 新增风险分析："
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. 清理与验收\n",
    "\n",
    "清理沙盒目录，运行验收检查。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 清理沙盒\n",
    "shutil.rmtree(SANDBOX_ROOT, ignore_errors=True)\n",
    "print(f'沙盒已清理: {SANDBOX_ROOT}')\n",
    "print()\n",
    "\n",
    "# 验收清单\n",
    "checks = {\n",
    "    '加载了工具 Schema': True,\n",
    "    '运行了正常调用流程': True,\n",
    "    '对比了\"模型声称\"与\"日志证明\"': True,\n",
    "    '运行了攻击测试': True,\n",
    "    '查看了审计日志': True,\n",
    "    '写了攻击分析': False,       # 检查第7节的分析单元格\n",
    "    '完成了B档修改': False,       # 检查第9节是否有修改\n",
    "}\n",
    "\n",
    "print('验收清单:')\n",
    "for check, status in checks.items():\n",
    "    print(f'  [{\"✓\" if status else \" \"}] {check}')\n",
    "\n",
    "print()\n",
    "print('请手动把 False 改为 True，确认你完成了对应项目。')\n",
    "print()\n",
    "print('关键概念检查:')\n",
    "print('  1. 工具调用中，模型的角色是 ___，宿主程序的角色是 ___')\n",
    "print('  2. JSON Schema 的作用是 ___')\n",
    "print('  3. 路径穿越攻击的原理是 ___，防御方法是 ___')\n",
    "print('  4. 审计日志的价值是 ___')\n",
    "print('  5. \"模型声称已创建\"和\"日志证明已执行\"的区别是 ___')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
