Hotdry.
ai-systems

ADK-JS 工具调用权限控制模型:运行时安全检查与多租户隔离

深入分析 ADK-JS 中工具调用的权限控制实现,包括身份授权模型、工具上下文验证、回调函数机制与多租户隔离策略。

随着 AI 代理能力的不断增强,确保其安全、可控地执行工具调用成为企业级应用的关键挑战。Google 开源的 Agent Development Kit for TypeScript (ADK-JS) 提供了一套完整的工具调用权限控制模型,通过多层安全机制实现精细化的访问控制。本文将深入分析 ADK-JS 中的权限控制实现,重点关注运行时安全检查、资源访问控制与多租户隔离机制。

工具调用权限控制的核心挑战

在 AI 代理系统中,工具调用权限控制面临三个核心挑战:身份边界模糊间接提示注入风险多租户隔离需求。ADK-JS 的安全文档明确指出,间接提示注入通过工具使用是主要风险源,恶意用户可能通过精心构造的输入绕过代理的初始指令,触发未经授权的工具操作。

风险类别包括目标错位与目标腐化、有害内容生成以及不安全操作。例如,代理可能误解复杂或模糊的指令,执行损害系统的命令,进行未经授权的购买或金融交易,甚至泄露敏感的个人数据。这些风险要求权限控制模型必须具备细粒度的验证能力和运行时监控机制。

身份与授权:Agent-Auth 与 User-Auth 的工程化选择

ADK-JS 提供了两种主要的身份授权模式,开发者需要根据具体场景做出工程化选择。

Agent-Auth 模式:代理身份的统一访问

在 Agent-Auth 模式下,工具使用代理自身的身份与外部系统交互(例如服务账户)。代理身份必须在外部系统的访问策略中明确授权,比如将代理的服务账户添加到数据库的 IAM 策略中授予只读权限。这种策略通过仅授予只读权限来约束代理,无论模型如何决策,工具都将被禁止执行写入操作。

适用场景:当所有用户共享相同的访问级别时,Agent-Auth 模式最为适用。例如,面向公众的客服代理,所有用户都只能查询产品信息而不能修改数据。

实现要点

  • 在工具实现中确保创建日志以维护操作到用户的归属关系
  • 所有代理操作都将显示为来自代理本身
  • 需要在外部系统中配置精细的 IAM 策略
// 概念示例:配置代理身份的工具
const queryTool = new DatabaseQueryTool({
  serviceAccount: 'agent-service-account@project.iam.gserviceaccount.com',
  permissions: ['readOnly']
});

User-Auth 模式:用户身份的委托访问

User-Auth 模式下,工具使用 "控制用户" 的身份与外部系统交互(例如与前端交互的人类用户)。在 ADK 中,这通常通过 OAuth 实现:代理与前端交互以获取 OAuth 令牌,然后工具在执行外部操作时使用该令牌。

安全优势:代理只能执行用户自己可以执行的操作,这大大降低了恶意用户滥用代理获取额外数据访问权限的风险。

局限性:大多数常见的委托实现具有固定的权限集(即 OAuth 范围)。通常,这些范围比代理实际需要的访问权限更广泛,需要额外的技术来进一步约束代理操作。

// 概念示例:使用用户 OAuth 令牌的工具
async function queryWithUserAuth(query: string, toolContext: ToolContext) {
  const userToken = toolContext.state.get('user_oauth_token');
  // 使用用户令牌执行查询
  return await executeQueryWithToken(query, userToken);
}

工具上下文与运行时安全检查机制

ADK-JS 的核心创新之一是 ** 工具上下文(Tool Context)** 机制,它允许开发者在工具执行时传递确定性的控制策略,实现运行时安全检查。

工具上下文的设计哲学

工具接收两种类型的输入:由模型设置的参数,以及由代理开发者确定性设置的工具上下文。通过依赖确定性设置的信息,可以验证模型是否按预期行为。

// 设置策略数据用于工具上下文
const policy: { [key: string]: any } = {};
policy['select_only'] = true;
policy['tables'] = ['mytable1', 'mytable2'];

// 存储在会话状态中供工具访问
invocationContext.session.state["query_tool_policy"] = policy;

运行时策略执行

在工具执行期间,工具上下文被传递给工具,开发者可以在工具内部实施策略验证:

function query(query: string, toolContext: ToolContext): string | object {
  const policy = toolContext.state.get('query_tool_policy', {}) as { [key: string]: any };
  
  // 策略执行:检查查询是否针对授权表
  const actual_tables = explainQuery(query); // 假设的函数调用
  
  const policyTables = new Set(policy['tables'] || []);
  const isSubset = actual_tables.every(table => policyTables.has(table));
  
  if (!isSubset) {
    const allowed = (policy['tables'] || ['(None defined)']).join(', ');
    return `Error: Query targets unauthorized tables. Allowed: ${allowed}`;
  }
  
  if (policy['select_only']) {
    if (!query.trim().toUpperCase().startsWith("SELECT")) {
      return "Error: Policy restricts queries to SELECT statements only.";
    }
  }
  
  // 执行验证后的查询
  return executeValidatedQuery(query);
}

这种设计实现了防御性工具设计原则:通过限制我们提供给代理的操作范围,可以确定性地消除我们绝不希望代理执行的恶意操作类别。

回调函数与插件系统的多租户隔离实现

对于更复杂的权限控制需求,ADK-JS 提供了回调函数和插件系统,支持可重用的安全策略和多租户隔离。

Before Tool Callback:工具调用前验证

当无法修改工具本身以添加护栏时,可以使用 Before Tool Callback 函数添加调用前的验证。回调函数可以访问代理状态、请求的工具和参数。

function validateToolParams({
  tool,
  args,
  context
}: {
  tool: BaseTool,
  args: { [key: string]: any },
  context: ToolContext
}): { [key: string]: any } | undefined {
  
  // 示例验证:检查状态中的必需用户ID是否与参数匹配
  const expectedUserId = context.state.get("session_user_id");
  const actualUserIdInArgs = args["user_id_param"]; // 假设工具接受'user_id_param'
  
  if (actualUserIdInArgs !== expectedUserId) {
    // 返回字典以阻止工具执行并提供反馈
    return { "error": `Tool call blocked: User ID mismatch.` };
  }
  
  // 返回 undefined 以允许工具调用继续
  return undefined;
}

// 在代理设置中分配回调
const rootAgent = new LlmAgent({
  model: 'gemini-2.5-flash',
  name: 'root_agent',
  instruction: "...",
  beforeToolCallback: validateToolParams,
  tools: [queryToolInstance]
});

这种方法非常通用,甚至可以创建可重用的工具策略库。然而,如果强制执行护栏所需的信息不能直接从参数中看到,它可能不适用于所有工具。

插件系统:可重用的安全策略

插件系统是实施不特定于单个代理的安全策略的推荐方法。插件设计为自包含和模块化,允许您为特定安全策略创建单独的插件,并在运行器级别全局应用它们。

多租户隔离实现:安全插件可以配置一次并应用于使用运行器的每个代理,确保整个应用程序中一致的安全护栏,无需重复代码。

Gemini 作为法官插件示例

此插件使用 Gemini Flash Lite 评估用户输入、工具输入和输出以及代理响应的适当性、提示注入和越狱检测:

class GeminiJudgePlugin implements SecurityPlugin {
  async validateInput(input: UserInput, context: PluginContext): Promise<ValidationResult> {
    const safetyCheck = await geminiFlashLite.evaluateSafety(input.content);
    
    if (safetyCheck.isUnsafe) {
      return {
        allowed: false,
        response: "抱歉,我无法帮助处理这个问题。我可以帮您处理其他事情吗?"
      };
    }
    
    return { allowed: true };
  }
  
  // 应用于工具调用前验证
  async beforeToolCall(tool: Tool, args: any): Promise<ToolValidationResult> {
    const toolSafety = await this.checkToolSafety(tool.name, args);
    return toolSafety;
  }
}

PII 脱敏插件示例

专门为 Before Tool Callback 设计的插件,用于在工具处理或发送到外部服务之前脱敏个人身份信息:

class PIIRedactionPlugin implements ToolCallbackPlugin {
  async processBeforeToolCall(
    tool: Tool,
    args: Record<string, any>,
    context: ToolContext
  ): Promise<ProcessedArgs> {
    const redactedArgs = { ...args };
    
    // 检测并脱敏 PII
    for (const [key, value] of Object.entries(args)) {
      if (typeof value === 'string') {
        redactedArgs[key] = this.redactPII(value);
      }
    }
    
    return redactedArgs;
  }
  
  private redactPII(text: string): string {
    // 实现 PII 检测和脱敏逻辑
    return text.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[CREDIT_CARD]')
               .replace(/\b\d{3}[\s-]?\d{2}[\s-]?\d{4}\b/g, '[SSN]');
  }
}

工程化实施指南与最佳实践

1. 分层安全策略设计

实施工具调用权限控制时,应采用分层安全策略:

  1. 身份层:根据场景选择 Agent-Auth 或 User-Auth
  2. 上下文层:通过工具上下文设置确定性策略
  3. 验证层:使用回调函数进行参数验证
  4. 插件层:应用可重用的安全插件
  5. 监控层:实施评估和追踪

2. 多租户隔离参数配置

对于多租户系统,需要配置以下关键参数:

const tenantAConfig = {
  toolPermissions: {
    databaseQuery: {
      allowedTables: ['tenantA_products', 'tenantA_users'],
      maxRows: 1000,
      timeoutMs: 5000
    },
    apiCall: {
      allowedEndpoints: ['/api/tenantA/*'],
      rateLimit: '100/分钟'
    }
  },
  securityPlugins: [
    new TenantIsolationPlugin('tenantA'),
    new PIIRedactionPlugin({ tenantId: 'tenantA' })
  ]
};

const tenantBConfig = {
  toolPermissions: {
    databaseQuery: {
      allowedTables: ['tenantB_data'],
      maxRows: 500,
      timeoutMs: 3000
    }
  }
};

3. 运行时监控与审计

实施全面的监控和审计机制:

  • 工具调用日志:记录所有工具调用,包括参数、上下文和结果
  • 异常检测:监控异常模式,如频繁的权限拒绝或参数验证失败
  • 性能指标:跟踪工具执行时间、成功率等指标
  • 审计追踪:维护完整的操作审计追踪,支持事后分析

4. 沙箱代码执行安全

对于代码执行工具,必须实施沙箱环境:

const codeExecutionTool = new SandboxedCodeExecutor({
  timeout: 10000, // 10秒超时
  memoryLimit: '256MB',
  networkAccess: false, // 禁止网络访问
  allowedModules: ['math', 'datetime', 'json'],
  cleanupPolicy: 'full' // 完全清理执行环境
});

总结与展望

ADK-JS 的工具调用权限控制模型通过多层安全机制提供了企业级的安全保障。从基础的身份授权模型到高级的工具上下文验证,再到可扩展的回调函数和插件系统,ADK-JS 为开发者提供了构建安全、可控的 AI 代理所需的完整工具集。

关键实施要点包括:

  1. 根据访问模式选择适当的身份模型:Agent-Auth 适用于统一访问,User-Auth 适用于个性化访问
  2. 充分利用工具上下文进行确定性控制:通过开发者设置的策略约束模型行为
  3. 实施分层验证机制:结合工具内验证和回调函数验证
  4. 采用插件架构实现可重用安全策略:特别是对于多租户系统
  5. 实施全面的监控和审计:确保可观察性和合规性

随着 AI 代理系统的复杂性不断增加,工具调用权限控制将继续演进。未来的发展方向可能包括更细粒度的动态权限管理、基于机器学习的异常检测以及跨代理系统的统一安全策略协调。ADK-JS 当前的实现为这些高级功能奠定了坚实的基础。

资料来源

  1. ADK-JS 安全文档:https://google.github.io/adk-docs/safety/
  2. ADK-JS GitHub 仓库:https://github.com/google/adk-js
查看归档