详细教程展示如何在Nuxt中集成AI SDK 7,借助Amazon Bedrock调用Claude模型,并实现文件删除前的操作授权确认机制。
我做了一个蠢事。我建了一个超级英雄主题的 Nuxt 应用,接入了一个通过 Amazon Bedrock 调用 Anthropic 模型的接口,还给了它一个能删除我电脑文件的工具。实际上,要是我稍有不慎,它可能把我所有文件都删光了!
第一次尝试的时候,我没有加任何审批机制。结果如你所料——它直接删了文件。后来我研究了一下自己的 Coding Agent 是怎么工作的,学到了工具审批(tool approval)这个机制。
我了解到 AI SDK 7 在模型调用层面有工具审批功能。它的工作方式是:暂停等待审批、弹出一个审批窗口,然后才执行删除。后来我还把 Kiro CLI 也接入了同一套界面,用的是 Agent Client Protocol(ACP)。
在 YouTube 上观看完整视频。
Node.js 22 或更高版本。AI SDK 7 需要 Node.js 22,并使用 ECMAScript Modules(ESM)。
npm 11 或其他能与 Nuxt 4 配合使用的包管理器。
AWS 凭证可通过标准 Provider 链获取。
在你所在的 AWS Region 中有权限访问 Amazon Bedrock 模型。
AWS CLI(如果你想列出账户可用的推理配置文件)。
已认证的 Kiro CLI 安装(用于可选的 ACP 部分)。
创建项目并安装录制演示时使用的版本:
npx nuxi@latest init nuxt-agent-approval
cd nuxt-agent-approval
npm install \
nuxt@4.5.2 \
vue@3.5.41 \
ai@7.0.66 \
@ai-sdk/vue@4.0.66 \
@ai-sdk/amazon-bedrock@5.0.57 \
@aws-sdk/credential-providers@3.1111.0 \
@nuxt/ui@4.10.0 \
zod@4.4.3
npm install -D @iconify-json/lucide@1.2.123
注册 Nuxt UI,并通过服务端运行时配置暴露 Amazon Bedrock 设置:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
runtimeConfig: {
awsRegion: process.env.AWS_REGION ?? 'us-west-2',
bedrockModelId: process.env.NUXT_BEDROCK_MODEL_ID
}
})
添加两个 Nuxt UI 导入:
/* app/assets/css/main.css */
@import "tailwindcss";
@import "@nuxt/ui";
你可以把自己的设置与完整的配套项目对比参考。
视频用到了两个 fixture 文件:old-draft.md 和 keep-me.md。在添加工具之前先把它们创建好:
mkdir -p fixtures
printf '# Old draft\n' > fixtures/old-draft.md
printf '# Keep me\n' > fixtures/keep-me.md
Agent 可以列出或删除该目录中的文件,但不应接受诸如 ../../package.json 这样的路径。
审批决定一个工具是否运行,但它不决定工具启动后能访问到哪些范围。
// server/utils/file-tools.ts
import { lstat, readdir, realpath, rm } from 'node:fs/promises'
import { resolve, relative, isAbsolute } from 'node:path'
import { tool } from 'ai'
import * as z from 'zod'
function resolveInsideFixtures(inputPath: string): string {
const root = resolve(process.cwd(), 'fixtures')
const target = resolve(root, inputPath)
const rel = relative(root, target)
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
throw createError({
statusCode: 400,
statusMessage: `Path escapes the fixtures directory: ${inputPath}`
})
}
return target
}
export const listFiles = tool({
description: 'List the files in the project fixtures directory.',
inputSchema: z.object({}),
execute: async () => {
const entries = await readdir(resolve(process.cwd(), 'fixtures'), {
withFileTypes: true
})
return { files: entries.filter(entry => entry.isFile()).map(entry => entry.name) }
}
})
export const deleteFile = tool({
description: 'Permanently delete one file from the fixtures directory.',
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => {
const target = resolveInsideFixtures(path)
const info = await lstat(target).catch(() => null)
if (!info?.isFile() || info.isSymbolicLink()) {
return { deleted: false, path, reason: 'Not a regular file' }
}
const root = await realpath(resolve(process.cwd(), 'fixtures'))
const canonicalTarget = await realpath(target)
const rel = relative(root, canonicalTarget)
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
throw createError({
statusCode: 400,
statusMessage: 'File resolves outside fixtures'
})
}
await rm(canonicalTarget)
return { deleted: true, path }
}
})
第二次检查发生在 realpath() 之后。这是为了捕获那些在解析前看起来是本地路径、但通过符号链接指向 fixture 目录之外的文件。
我用的是 Amazon Bedrock,但你可以用任意支持 AI SDK 的 Provider。
在 server/utils/bedrock.ts 中创建 Provider:
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
export function useBedrock() {
const { awsRegion } = useRuntimeConfig()
return createAmazonBedrock({
region: awsRegion,
credentialProvider: fromNodeProviderChain()
})
}
fromNodeProviderChain() 使用本地环境已有的 AWS 凭证,包括 AWS IAM Identity Center 会话、命名配置文件、环境变量和实例角色。你不需要在 Nuxt 项目里放一个长期有效的访问密钥。
不要复制这篇文章里的模型 ID。可用的 ID 因账户和 AWS Region 而异。列出你账户的活跃 Amazon Bedrock 推理配置文件:
aws bedrock list-inference-profiles \
--region us-west-2 \
--query 'inferenceProfileSummaries[?status==`ACTIVE`].inferenceProfileId'
在启动 Nuxt 之前设置其中一个返回的配置文件 ID:
export AWS_REGION=us-west-2
read -r -p "Inference profile ID: " NUXT_BEDROCK_MODEL_ID
export NUXT_BEDROCK_MODEL_ID
npm run dev
现在添加一个没有保护的聊天路由。一开始不加审批是为了让失败变得可见:
// server/api/chat.post.ts
import {
streamText,
stepCountIs,
convertToModelMessages,
toUIMessageStream,
createUIMessageStreamResponse
} from 'ai'
import type { AmazonBedrockProvider } from '@ai-sdk/amazon-bedrock'
type BedrockModelId = Parameters<AmazonBedrockProvider>[0]
export default defineEventHandler(async event => {
const { messages } = await readBody(event)
const { bedrockModelId } = useRuntimeConfig()
const bedrock = useBedrock()
const result = streamText({
model: bedrock(bedrockModelId as BedrockModelId),
instructions:
'Manage files in the fixture project. List files before deleting. Never guess a filename.',
messages: await convertToModelMessages(messages),
tools: { listFiles, deleteFile },
stopWhen: stepCountIs(5)
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
stopWhen 很重要。AI SDK 7 默认在一步之后就停止。模型可以调用 deleteFile、收到结果,然后在告知用户发生了什么之前就停止。五步的限制为列出、删除和总结留出了空间,同时保持了循环有界。
到了这一步,delete old-draft.md 会在模型选中该工具后立即删除文件。这就是视频第一分钟里发生的事情。
给 streamText() 调用加一个选项(toolApproval):
const result = streamText({
model: bedrock(bedrockModelId as BedrockModelId),
instructions:
'Manage files in the fixture project. List files before deleting. Never guess a filename.',
messages: await convertToModelMessages(messages),
tools: { listFiles, deleteFile },
stopWhen: stepCountIs(5),
toolApproval: {
deleteFile: 'user-approval'
}
})
这个选项在 streamText() 上,而不是在工具定义内部。同一个 deleteFile 工具在维护任务中可能无人值守运行,而在面向客户的聊天中则需要人工介入。
AI SDK 7 支持比「是或否」更复杂的策略。策略函数可以批准调用、直接拒绝而无需询问,或者发送给用户。本例使用直接 user-approval 状态是因为每次删除都应该停下来。
Nuxt UI 的聊天文档遵循相同的 AI SDK 消息部件模型。录制的应用使用了 @ai-sdk/vue 的 useChat() 和 Nuxt UI 的 isToolApprovalPending() 辅助函数:
<script setup lang="ts">
import {
DefaultChatTransport,
getToolName,
isTextUIPart,
isToolUIPart,
lastAssistantMessageIsCompleteWithApprovalResponses
} from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isToolApprovalPending } from '@nuxt/ui/utils/ai'
const input = ref('')
const {
messages,
status,
sendMessage,
addToolApprovalResponse
} = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses
})
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
</script>
sendAutomaticallyWhen 用于在用户响应后恢复中断的轮次。没有它,审批状态会在浏览器中改变,但 Agent 不会自动继续。
在审批等待期间渲染工具部件并附加两个决定:
<template>
<div v-for="message in messages" :key="message.id">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<div v-if="isToolUIPart(part)" class="tool-card">
<strong>{{ getToolName(part) }}</strong>
<pre v-if="part.input">{{ part.input }}</pre>
<div v-if="isToolApprovalPending(part)">
<button
type="button"
@click="addToolApprovalResponse({
id: part.approval!.id,
approved: true
})"
>
Do it
</button>
<button
type="button"
@click="addToolApprovalResponse({
id: part.approval!.id,
approved: false,
reason: 'The user declined this file deletion.'
})"
>
Nope
</button>
</div>
</div>
<p v-else-if="isTextUIPart(part)">{{ part.text }}</p>
</template>
</div>
<form @submit.prevent="onSubmit">
<input v-model="input" placeholder="delete old-draft.md">
<button :disabled="status !== 'ready'">Send</button>
</form>
</template>
顺带一提,当用户看不到 Agent 想删除哪个文件时,审批按钮是没有用的。
拒绝理由也很有用。approved: false 告诉模型不能运行该工具。理由给了它足够的上下文来解释发生了什么,而不是再次尝试相同的调用。
运行提示词两次。一次拒绝,确认 old-draft.md 还在。重置 fixtures,重复提示词,批准它,确认文件消失了。模型两次收到的是同一个请求。
视频在最后的演示中切换了后端。不再通过 Amazon Bedrock 直接向 Anthropic 模型发送提示词,Nuxt 应用改为通过 Agent Client Protocol 与 Kiro CLI 通信。
ACP 给应用提供了一种通用方式来启动 Agent 会话、发送提示词、接收工具事件,以及回答权限请求。Kiro 作为独立进程运行,保留其现有的 Agent 工具和 Model Context Protocol(MCP)集成。Nuxt 应用仍然负责界面和自己暴露的主机工具。

安装 AI SDK Harness 包:
npm install \
@ai-sdk/harness@1.0.73 \
@ai-sdk/harness-acp@1.0.11
Kiro 路由创建的是 HarnessAgent 而不是直接调用 streamText()。createKiroHarness() ACP 预设和 createUnsafeLocalSandbox() 开发适配器来自配套项目,所以这里把它当作路由配置而不是独立文件:
import { execFileSync } from 'node:child_process'
import { HarnessAgent } from '@ai-sdk/harness/agent'
const kiroExecutable = execFileSync('which', ['kiro-cli'], {
encoding: 'utf-8'
}).trim()
const agent = new HarnessAgent({
harness: createKiroHarness({ port: 4100 }),
sandbox: createUnsafeLocalSandbox({
ports: [4100],
hostBins: [
{ harnessId: 'kiro', name: 'kiro-cli', target: kiroExecutable }
]
}),
permissionMode: 'allow-reads',
instructions:
'Manage files in the fixture project. List files before deleting. Never guess a filename.',
tools: { listFiles, deleteFile },
toolApproval: {
deleteFile: 'user-approval'
}
})
完整的 Kiro 路由和 ACP 预设包括会话创建和审批延续。当浏览器返回响应时,路由收集待处理的审批响应,然后对 Kiro 会话调用 continueStream()。
我录制运行时,在删除 keep-me.md 之前问了不止一次。Kiro 确认了目标,它的权限流程请求运行该工具,而主机 deleteFile 策略请求最终审批。这有点重复,但它暴露了一条重要的边界。Kiro 内置的权限和 AI SDK 的主机工具审批是两个独立的系统。
本例中的本地沙箱是一个开发适配器。它把文件 API 路径限制在临时根目录,但进程仍然以当前操作系统用户身份运行。在向不受信任的提示词开放 Coding Agent 之前,应该用隔离的沙箱 Provider 替换它。
工具审批是产品 DX 改进,不是安全加固。
如果一个人批准了错误的路径,工具仍然会删除错误的路径。如果工具能访问文件系统其余部分,审批并不会收窄那个访问范围。保留步骤 2 中的路径检查、在工具内部应用授权、在服务器上验证输入,并对能运行命令的 Agent 使用隔离运行时。
示例应用也把录制控制保留在开发模式中。浏览器可以在第一次演示时关闭审批,但构建出的应用会忽略那个标志并要求审批。一个由客户端控制的开关来禁用确认是不应该发布的。
停止 Nuxt 开发服务器。如果在演示过程中删除了任一 fixture 文件,在下次运行前重新创建这两个文件:
printf '# Old draft\n' > fixtures/old-draft.md
printf '# Keep me\n' > fixtures/keep-me.md
本教程不提供 AWS 资源。Amazon Bedrock 请求仍可能产生费用,所以在测试完成后停止发送提示词。
如果你删除了本地项目目录,它的文件以及任何本地会话状态都会被移除。在删除之前复制任何你想保留的东西。通过 Provider 链加载的 AWS 凭证保留在它们原始的配置文件或 Identity Center 缓存中;这个应用不会把它们写入项目。
通过添加这个工具审批流程,我学到了很多东西。尽管我通常使用 Strands Agents,但 AI SDK 7 真的很好用。
如果看到这里,一定要留言评论!
AI SDK 7 公告
Nuxt UI 聊天组件
Kiro CLI ACP 文档
Amazon Bedrock 跨 Region 推理