#!/usr/bin/env node /** * agent-claim-observer.js — P0 被动审计观察者 (v6.6-rc2) * AGENT_CLAIM_OBSERVER_P0 * * SubagentStop 钩子 · 纯观察 · 不阻断 · fail-open * 记录 Agent 返回元数据到 debug/agent-returns.jsonl * 字段: {ts, traceId, session_id, agent_type, agent_id, text_length, pid} * * P0 观察期目标: 累积真实 Agent 返回数据, 为 P1 verifier 决策提供基线. */ 'use strict'; const fs = require('fs'); const path = require('path'); let CLAUDE_ROOT; try { CLAUDE_ROOT = require('./lib/root.js'); } catch { CLAUDE_ROOT = path.dirname(__dirname); } const LOG_FILE = path.join(CLAUDE_ROOT, 'debug', 'agent-returns.jsonl'); const WATCHDOG_MS = 3000; function readStdinSync() { try { return JSON.parse(fs.readFileSync(0, 'utf8')); } catch { return null; } } function extractText(tr) { if (!tr) return ''; if (typeof tr === 'string') return tr; if (typeof tr.content === 'string') return tr.content; if (Array.isArray(tr.content)) { return tr.content .filter(b => b && b.type === 'text' && typeof b.text === 'string') .map(b => b.text) .join('\n'); } return ''; } function extractTraceId(input, text) { // 正则有界 (8-64 hex-like), 防 ReDoS const re = /(bwr-[A-Za-z0-9-]{8,64})<\/trace>/; const prompt = (input && input.tool_input && input.tool_input.prompt) || ''; const m1 = re.exec(prompt); if (m1) return m1[1]; const m2 = re.exec(text); if (m2) return m2[1]; return ''; } function main() { const wdt = setTimeout(() => process.exit(0), WATCHDOG_MS); wdt.unref(); try { const input = readStdinSync(); if (input) { const text = extractText(input.tool_response); const traceId = extractTraceId(input, text); const record = { ts: new Date().toISOString(), traceId: traceId, session_id: input.session_id || '', agent_type: (input.tool_input && input.tool_input.subagent_type) || '', agent_id: input.agent_id || input.tool_use_id || '', text_length: text.length, pid: process.pid, }; try { fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true }); } catch {} fs.appendFileSync(LOG_FILE, JSON.stringify(record) + '\n'); } } catch { // fail-open: 观察者异常不阻断主流程 } clearTimeout(wdt); process.exit(0); } if (require.main === module) main(); module.exports = { extractText, extractTraceId };