94 lines
2.8 KiB
JavaScript
94 lines
2.8 KiB
JavaScript
|
|
#!/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 = /<trace>(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,
|
||
|
|
// [P0-1] METRICS_EMIT_v1
|
||
|
|
model: (input.tool_input && input.tool_input.model) || '',
|
||
|
|
tool_name: (input.tool_input && input.tool_input.name) || 'Agent',
|
||
|
|
duration_ms: (input.tool_response && input.tool_response.duration_ms) || null,
|
||
|
|
pid: process.pid,
|
||
|
|
};
|
||
|
|
try { fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true }); } catch {}
|
||
|
|
fs.appendFileSync(LOG_FILE, JSON.stringify(record) + '\n');
|
||
|
|
// [P0-1] METRICS_EMIT_v1 — 指标发射
|
||
|
|
try { require('./lib/metrics.js').emit('agent', record); } catch {}
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// fail-open: 观察者异常不阻断主流程
|
||
|
|
}
|
||
|
|
clearTimeout(wdt);
|
||
|
|
process.exit(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (require.main === module) main();
|
||
|
|
|
||
|
|
module.exports = { extractText, extractTraceId };
|