watchmyagents 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/agents.js +4 -128
- package/scripts/fetch-anthropic.js +26 -3
- package/src/typology-features.js +136 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "watchmyagents",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Security observability + real-time policy enforcement for AI agents. Local-first NDJSON capture with a continuous Watch daemon that auto-uploads anonymized signals, Shield CLI that blocks policy violations live (with policies pulled from Fortress cloud), anonymizer producing signals-only payloads, bidirectional sync with WatchMyAgents Fortress, and one-command install as an always-on launchd/systemd service — closing the recursive Watch→Guardian→Shield security loop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/scripts/agents.js
CHANGED
|
@@ -18,12 +18,10 @@
|
|
|
18
18
|
// ANTHROPIC_API_KEY from env (or --api-key, discouraged).
|
|
19
19
|
|
|
20
20
|
import os from 'node:os';
|
|
21
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
22
|
-
import { createReadStream } from 'node:fs';
|
|
23
|
-
import { createInterface } from 'node:readline';
|
|
24
21
|
import { join, resolve } from 'node:path';
|
|
25
22
|
import { listAgents } from '../src/sources/anthropic-managed.js';
|
|
26
23
|
import { classifyAgentType } from '../src/typology.js';
|
|
24
|
+
import { aggregate, buildFeatures, NON_DERIVABLE } from '../src/typology-features.js';
|
|
27
25
|
import { isValidAgentId, assertSafePathSegment } from '../src/validate.js';
|
|
28
26
|
|
|
29
27
|
function parseArgs(argv) {
|
|
@@ -40,131 +38,9 @@ function parseArgs(argv) {
|
|
|
40
38
|
function die(msg, code = 1) { process.stderr.write(`error: ${msg}\n`); process.exit(code); }
|
|
41
39
|
function info(msg) { process.stdout.write(`[wma-agents] ${msg}\n`); }
|
|
42
40
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
48
|
-
// Tool-name → category mapping (Modèle C: name-based, no content). Managed
|
|
49
|
-
// Agents expose tools as an opaque bundle, so tool_name is free-text. We match
|
|
50
|
-
// the confirmed built-ins (web_search, web_fetch, bash) plus best-effort
|
|
51
|
-
// regexes for common tool names. A tool that matches nothing contributes to the
|
|
52
|
-
// denominator but to no category (honest: unknown ≠ inferred).
|
|
53
|
-
// ──────────────────────────────────────────────────────────────────────────
|
|
54
|
-
const CATEGORY_RULES = [
|
|
55
|
-
// category, matcher (lower-cased tool_name)
|
|
56
|
-
['search', (n) => /(^|_)web_search$|(^|_)search($|_)|google|brave/.test(n)],
|
|
57
|
-
['browser', (n) => /web_fetch|browser|playwright|puppeteer|navigate|screenshot/.test(n)],
|
|
58
|
-
['http', (n) => /(^|_)http|fetch_url|curl|request|webhook|api_call/.test(n)],
|
|
59
|
-
['code', (n) => /bash|shell|terminal|code_exec|exec_|python|node_run|run_code|interpreter/.test(n)],
|
|
60
|
-
['database', (n) => /sql|query_db|database|postgres|mysql|mongo|redis|bigquery|snowflake/.test(n)],
|
|
61
|
-
['email', (n) => /email|gmail|smtp|sendmail|mailgun|outlook/.test(n)],
|
|
62
|
-
['payment', (n) => /payment|charge|transfer|invoice|stripe|paypal|payout|refund|checkout/.test(n)],
|
|
63
|
-
['secret', (n) => /secret|vault|credential|kms|keychain|token_get/.test(n)],
|
|
64
|
-
['memory', (n) => /memory|retriev|vector|(^|_)rag($|_)|knowledge|embed|pinecone|chroma/.test(n)],
|
|
65
|
-
['file', (n) => /editor|str_replace|read_file|write_file|create_file|file_io|(^|_)file($|_)|fs_/.test(n)],
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
// Best-effort deploy detection (spec discriminator devops_infra vs coding).
|
|
69
|
-
const DEPLOY_RE = /deploy|terraform|kubectl|helm|(^|_)release($|_)|ansible|pulumi|cloudformation/;
|
|
70
|
-
|
|
71
|
-
function categoryOf(toolName) {
|
|
72
|
-
const n = String(toolName || '').toLowerCase();
|
|
73
|
-
for (const [cat, m] of CATEGORY_RULES) if (m(n)) return cat;
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Aggregate raw counts from an agent's local NDJSON logs (Modèle C: counts only).
|
|
78
|
-
async function aggregate(logDir, agentId) {
|
|
79
|
-
const actionCounts = {}; // action_type → count
|
|
80
|
-
const categoryCounts = {}; // tool category → count
|
|
81
|
-
let toolEvents = 0; // denominator for f_* fractions
|
|
82
|
-
let deployUses = 0;
|
|
83
|
-
const dir = join(logDir, agentId);
|
|
84
|
-
const s = await stat(dir).catch(() => null);
|
|
85
|
-
if (!s || !s.isDirectory()) return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false };
|
|
86
|
-
let names;
|
|
87
|
-
try { names = await readdir(dir); } catch { return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false }; }
|
|
88
|
-
const files = names.filter((n) => n.endsWith('.ndjson') && !n.startsWith('raw-'));
|
|
89
|
-
if (files.length === 0) return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false };
|
|
90
|
-
|
|
91
|
-
for (const f of files) {
|
|
92
|
-
await new Promise((res) => {
|
|
93
|
-
const rl = createInterface({ input: createReadStream(join(dir, f), { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
94
|
-
rl.on('line', (line) => {
|
|
95
|
-
if (!line.trim()) return;
|
|
96
|
-
let e; try { e = JSON.parse(line); } catch { return; }
|
|
97
|
-
if (e.action_type) actionCounts[e.action_type] = (actionCounts[e.action_type] || 0) + 1;
|
|
98
|
-
if (TOOL_ACTIONS.has(e.action_type)) {
|
|
99
|
-
toolEvents += 1;
|
|
100
|
-
const cat = categoryOf(e.tool_name);
|
|
101
|
-
if (cat) categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
|
102
|
-
if (DEPLOY_RE.test(String(e.tool_name || '').toLowerCase())) deployUses += 1;
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
rl.on('close', res); rl.on('error', res);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: true };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Features that the WMA NDJSON logs CANNOT reliably expose today (opaque tool
|
|
112
|
-
// names / no behavioural signal / content off-limits under Modèle C). They
|
|
113
|
-
// default to 0/false; the caller prints a one-line note.
|
|
114
|
-
const NON_DERIVABLE = [
|
|
115
|
-
'f_database', 'f_email', 'f_payment', 'f_secret', 'f_memory',
|
|
116
|
-
'flag_internal_sys', 'flag_on_behalf', 'aux_untrusted', 'aux_sensitive',
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
// Build the canonical anonymized FEATURE VECTOR from the aggregated counts.
|
|
120
|
-
// Fractions = category_count / toolEvents. n_events = total observed events.
|
|
121
|
-
function buildFeatures(agg) {
|
|
122
|
-
const { actionCounts, categoryCounts, toolEvents, deployUses } = agg;
|
|
123
|
-
const nEvents = Object.values(actionCounts).reduce((a, b) => a + b, 0);
|
|
124
|
-
const frac = (c) => (toolEvents > 0 ? (categoryCounts[c] || 0) / toolEvents : 0);
|
|
125
|
-
const eventFrac = (...types) => (nEvents > 0
|
|
126
|
-
? types.reduce((a, t) => a + (actionCounts[t] || 0), 0) / nEvents
|
|
127
|
-
: 0);
|
|
128
|
-
|
|
129
|
-
// f_handoff / f_user_msg are derived from event TYPE (not tool category):
|
|
130
|
-
// confirmed action_types thread_message_* and user_message.
|
|
131
|
-
const handoff = eventFrac('thread_message_sent', 'thread_message_received', 'thread_created');
|
|
132
|
-
const userMsg = eventFrac('user_message');
|
|
133
|
-
|
|
134
|
-
// aux_autonomy ≈ 1 − (human-in-the-loop event share). Confirmed action_types
|
|
135
|
-
// user_message / user_interrupt / tool_confirmation mark human involvement; an
|
|
136
|
-
// agent that proceeds without them is more autonomous. Heuristic — documented.
|
|
137
|
-
const hitlShare = eventFrac('user_message', 'user_interrupt', 'tool_confirmation');
|
|
138
|
-
const auxAutonomy = nEvents > 0 ? Math.max(0, 1 - hitlShare) : 0;
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
// tool-category fractions (over tool uses)
|
|
142
|
-
f_code: frac('code'),
|
|
143
|
-
f_browser: frac('browser'),
|
|
144
|
-
f_database: frac('database'), // non-derivable in practice → ~0
|
|
145
|
-
f_http: frac('http'),
|
|
146
|
-
f_email: frac('email'), // non-derivable in practice → ~0
|
|
147
|
-
f_payment: frac('payment'), // non-derivable in practice → ~0
|
|
148
|
-
f_secret: frac('secret'), // non-derivable in practice → ~0
|
|
149
|
-
f_search: frac('search'),
|
|
150
|
-
f_memory: frac('memory'), // non-derivable in practice → ~0
|
|
151
|
-
f_file: frac('file'),
|
|
152
|
-
// event-type fractions (over all events)
|
|
153
|
-
f_handoff: handoff,
|
|
154
|
-
f_user_msg: userMsg,
|
|
155
|
-
// discriminator flags (best-effort; only flag_deploy has any behavioural
|
|
156
|
-
// signal — and only if the agent literally names a deploy tool).
|
|
157
|
-
flag_deploy: deployUses > 0 ? 1 : 0,
|
|
158
|
-
flag_internal_sys: 0, // no behavioural signal in logs
|
|
159
|
-
flag_on_behalf: 0, // no behavioural signal in logs
|
|
160
|
-
// aux ratios
|
|
161
|
-
aux_autonomy: auxAutonomy, // heuristic (HITL-frequency)
|
|
162
|
-
aux_untrusted: 0, // no honest source in logs
|
|
163
|
-
aux_sensitive: 0, // no honest source in logs
|
|
164
|
-
// window size
|
|
165
|
-
n_events: nEvents,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
41
|
+
// Feature aggregation lives in src/typology-features.js (shared with the Watch
|
|
42
|
+
// daemon so both CLI snapshot and continuous upload use the same Modèle C
|
|
43
|
+
// extraction). The rest of this file is just CLI presentation.
|
|
168
44
|
|
|
169
45
|
async function main() {
|
|
170
46
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -30,6 +30,8 @@ import { TokenTracker } from '../src/tokens.js';
|
|
|
30
30
|
import { SignalsAggregator } from '../src/anonymizer.js';
|
|
31
31
|
import { resolveFortressBase, fortressEndpoint } from '../src/fortress/url.js';
|
|
32
32
|
import { isValidAgentId, isValidSessionId, assertSafePathSegment } from '../src/validate.js';
|
|
33
|
+
import { classifyAgentType } from '../src/typology.js';
|
|
34
|
+
import { aggregate, buildFeatures } from '../src/typology-features.js';
|
|
33
35
|
import {
|
|
34
36
|
getAgent, listAgents, listSessions, fetchSessionEntries, fetchRawEvents,
|
|
35
37
|
} from '../src/sources/anthropic-managed.js';
|
|
@@ -105,7 +107,10 @@ function postJson(url, headers, body) {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
// Anonymize a batch of just-written entries and ship them as one signals row.
|
|
108
|
-
|
|
110
|
+
// `classification` (optional) carries the agent's typology — Fortress upserts
|
|
111
|
+
// agent_type/confidence/stage on the agent row so the typology badge + the
|
|
112
|
+
// apply-template flow fill themselves with no manual click.
|
|
113
|
+
async function uploadSignals(uploadCtx, agentId, displayName, entries, classification) {
|
|
109
114
|
const agg = new SignalsAggregator({ salt: uploadCtx.salt });
|
|
110
115
|
for (const e of entries) agg.add(e);
|
|
111
116
|
const sig = agg.finalize();
|
|
@@ -116,6 +121,7 @@ async function uploadSignals(uploadCtx, agentId, displayName, entries) {
|
|
|
116
121
|
window_start: sig.window_start,
|
|
117
122
|
window_end: sig.window_end,
|
|
118
123
|
payload: sig.payload,
|
|
124
|
+
...(classification ? { classification } : {}),
|
|
119
125
|
});
|
|
120
126
|
const { status, body: resp } = await postJson(
|
|
121
127
|
uploadCtx.url, { authorization: `Bearer ${uploadCtx.apiKey}` }, body,
|
|
@@ -217,6 +223,8 @@ async function runWatch({ apiKey, resolveAgents, fleet, logDir, intervalMs, wind
|
|
|
217
223
|
const loggers = new Map(); // sessionId → Logger (session ids are globally unique)
|
|
218
224
|
const ended = new Set(); // terminated sessions (skip)
|
|
219
225
|
const sessionAgent = new Map();// sessionId → { agentId, model, displayName }
|
|
226
|
+
const priors = new Map(); // agentId → previous classification (threads the
|
|
227
|
+
// typology state machine across upload cycles)
|
|
220
228
|
|
|
221
229
|
const ac = new AbortController();
|
|
222
230
|
const shutdown = () => { info('shutting down…'); ac.abort(); };
|
|
@@ -274,8 +282,23 @@ async function runWatch({ apiKey, resolveAgents, fleet, logDir, intervalMs, wind
|
|
|
274
282
|
|
|
275
283
|
if (uploadCtx) {
|
|
276
284
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
285
|
+
// Compute the agent's typology from its CUMULATIVE local logs and
|
|
286
|
+
// thread the prior across cycles so the state machine refines toward
|
|
287
|
+
// stable (Modèle C: features = counts/categories only, no raw content).
|
|
288
|
+
let classification;
|
|
289
|
+
try {
|
|
290
|
+
const features = buildFeatures(await aggregate(logDir, ag.agentId));
|
|
291
|
+
features.agent_id = ag.agentId;
|
|
292
|
+
const cls = classifyAgentType(features, priors.get(ag.agentId) || null);
|
|
293
|
+
priors.set(ag.agentId, cls);
|
|
294
|
+
classification = { agent_type: cls.classified_type, confidence: cls.confidence, stage: cls.stage };
|
|
295
|
+
} catch (e) { warn(` classification skipped: ${e.message}`); }
|
|
296
|
+
|
|
297
|
+
const resp = await uploadSignals(uploadCtx, ag.agentId, sendNames ? ag.displayName : ag.agentId, fresh, classification);
|
|
298
|
+
if (resp?.signal_id) {
|
|
299
|
+
const cTag = classification ? ` · type ${classification.agent_type} (${Math.round(classification.confidence * 100)}%, ${classification.stage})` : '';
|
|
300
|
+
info(` ↑ signals uploaded (signal_id ${resp.signal_id})${cTag}`);
|
|
301
|
+
}
|
|
279
302
|
} catch (e) { warn(` signals upload failed: ${e.message}`); }
|
|
280
303
|
}
|
|
281
304
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Shared local-log feature extraction for the typology classifier (Modèle C).
|
|
2
|
+
// Both wma-agents (CLI snapshot) and the Watch daemon (continuous upload) use
|
|
3
|
+
// this to derive the anonymized behavioural FEATURE VECTOR from local NDJSON
|
|
4
|
+
// logs, then feed it to classifyAgentType() in ./typology.js.
|
|
5
|
+
//
|
|
6
|
+
// Modèle C invariant: only `action_type` and `tool_name` are read from each log
|
|
7
|
+
// line — the raw payload fields (input/output/content/error/thinking) are NEVER
|
|
8
|
+
// touched here, so no raw content can ever enter a feature.
|
|
9
|
+
|
|
10
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
11
|
+
import { createReadStream } from 'node:fs';
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
// Action types that represent a TOOL invocation (denominator for f_* fractions).
|
|
16
|
+
const TOOL_ACTIONS = new Set(['tool_use', 'mcp_tool_use', 'custom_tool_use']);
|
|
17
|
+
|
|
18
|
+
// Tool-name → category mapping. Anthropic Managed Agents expose tools as an
|
|
19
|
+
// opaque bundle, so tool_name is free-text. We match the confirmed built-ins
|
|
20
|
+
// (web_search, web_fetch, bash) plus best-effort regexes for common tool names.
|
|
21
|
+
// A tool that matches nothing contributes to the denominator but to no category
|
|
22
|
+
// (honest: unknown ≠ inferred).
|
|
23
|
+
const CATEGORY_RULES = [
|
|
24
|
+
['search', (n) => /(^|_)web_search$|(^|_)search($|_)|google|brave/.test(n)],
|
|
25
|
+
['browser', (n) => /web_fetch|browser|playwright|puppeteer|navigate|screenshot/.test(n)],
|
|
26
|
+
['http', (n) => /(^|_)http|fetch_url|curl|request|webhook|api_call/.test(n)],
|
|
27
|
+
['code', (n) => /bash|shell|terminal|code_exec|exec_|python|node_run|run_code|interpreter/.test(n)],
|
|
28
|
+
['database', (n) => /sql|query_db|database|postgres|mysql|mongo|redis|bigquery|snowflake/.test(n)],
|
|
29
|
+
['email', (n) => /email|gmail|smtp|sendmail|mailgun|outlook/.test(n)],
|
|
30
|
+
['payment', (n) => /payment|charge|transfer|invoice|stripe|paypal|payout|refund|checkout/.test(n)],
|
|
31
|
+
['secret', (n) => /secret|vault|credential|kms|keychain|token_get/.test(n)],
|
|
32
|
+
['memory', (n) => /memory|retriev|vector|(^|_)rag($|_)|knowledge|embed|pinecone|chroma/.test(n)],
|
|
33
|
+
['file', (n) => /editor|str_replace|read_file|write_file|create_file|file_io|(^|_)file($|_)|fs_/.test(n)],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Best-effort deploy detection (spec discriminator devops_infra vs coding).
|
|
37
|
+
const DEPLOY_RE = /deploy|terraform|kubectl|helm|(^|_)release($|_)|ansible|pulumi|cloudformation/;
|
|
38
|
+
|
|
39
|
+
// Features that the WMA NDJSON logs CANNOT reliably expose today (opaque tool
|
|
40
|
+
// names, no behavioural signal, or raw content off-limits under Modèle C).
|
|
41
|
+
// They default to 0/false; callers can surface this to the user.
|
|
42
|
+
export const NON_DERIVABLE = [
|
|
43
|
+
'f_database', 'f_email', 'f_payment', 'f_secret', 'f_memory',
|
|
44
|
+
'flag_internal_sys', 'flag_on_behalf', 'aux_untrusted', 'aux_sensitive',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function categoryOf(toolName) {
|
|
48
|
+
const n = String(toolName || '').toLowerCase();
|
|
49
|
+
for (const [cat, m] of CATEGORY_RULES) if (m(n)) return cat;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Aggregate raw counts from an agent's local NDJSON logs. Returns `hasLogs:false`
|
|
54
|
+
// when there's nothing to read (cold start).
|
|
55
|
+
export async function aggregate(logDir, agentId) {
|
|
56
|
+
const actionCounts = {};
|
|
57
|
+
const categoryCounts = {};
|
|
58
|
+
let toolEvents = 0;
|
|
59
|
+
let deployUses = 0;
|
|
60
|
+
const dir = join(logDir, agentId);
|
|
61
|
+
const s = await stat(dir).catch(() => null);
|
|
62
|
+
if (!s || !s.isDirectory()) return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false };
|
|
63
|
+
let names;
|
|
64
|
+
try { names = await readdir(dir); } catch { return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false }; }
|
|
65
|
+
const files = names.filter((n) => n.endsWith('.ndjson') && !n.startsWith('raw-'));
|
|
66
|
+
if (files.length === 0) return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: false };
|
|
67
|
+
|
|
68
|
+
for (const f of files) {
|
|
69
|
+
await new Promise((res) => {
|
|
70
|
+
const rl = createInterface({ input: createReadStream(join(dir, f), { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
71
|
+
rl.on('line', (line) => {
|
|
72
|
+
if (!line.trim()) return;
|
|
73
|
+
let e; try { e = JSON.parse(line); } catch { return; }
|
|
74
|
+
if (e.action_type) actionCounts[e.action_type] = (actionCounts[e.action_type] || 0) + 1;
|
|
75
|
+
if (TOOL_ACTIONS.has(e.action_type)) {
|
|
76
|
+
toolEvents += 1;
|
|
77
|
+
const cat = categoryOf(e.tool_name);
|
|
78
|
+
if (cat) categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
|
79
|
+
if (DEPLOY_RE.test(String(e.tool_name || '').toLowerCase())) deployUses += 1;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
rl.on('close', res); rl.on('error', res);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return { actionCounts, categoryCounts, toolEvents, deployUses, hasLogs: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Build the canonical anonymized FEATURE VECTOR from the aggregated counts.
|
|
89
|
+
// fractions f_* = category_count / toolEvents ; event fractions = type_count /
|
|
90
|
+
// nEvents ; flags 0/1 ; aux ratios in [0,1] ; n_events = total observed events.
|
|
91
|
+
export function buildFeatures(agg) {
|
|
92
|
+
const { actionCounts, categoryCounts, toolEvents, deployUses } = agg;
|
|
93
|
+
const nEvents = Object.values(actionCounts).reduce((a, b) => a + b, 0);
|
|
94
|
+
const frac = (c) => (toolEvents > 0 ? (categoryCounts[c] || 0) / toolEvents : 0);
|
|
95
|
+
const eventFrac = (...types) => (nEvents > 0
|
|
96
|
+
? types.reduce((a, t) => a + (actionCounts[t] || 0), 0) / nEvents
|
|
97
|
+
: 0);
|
|
98
|
+
|
|
99
|
+
// f_handoff / f_user_msg are derived from event TYPE (not tool category):
|
|
100
|
+
// confirmed action_types thread_message_* and user_message.
|
|
101
|
+
const handoff = eventFrac('thread_message_sent', 'thread_message_received', 'thread_created');
|
|
102
|
+
const userMsg = eventFrac('user_message');
|
|
103
|
+
|
|
104
|
+
// aux_autonomy ≈ 1 − (human-in-the-loop event share). user_message /
|
|
105
|
+
// user_interrupt / tool_confirmation mark human involvement.
|
|
106
|
+
const hitlShare = eventFrac('user_message', 'user_interrupt', 'tool_confirmation');
|
|
107
|
+
const auxAutonomy = nEvents > 0 ? Math.max(0, 1 - hitlShare) : 0;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
// tool-category fractions (over tool uses)
|
|
111
|
+
f_code: frac('code'),
|
|
112
|
+
f_browser: frac('browser'),
|
|
113
|
+
f_database: frac('database'), // non-derivable in practice → ~0
|
|
114
|
+
f_http: frac('http'),
|
|
115
|
+
f_email: frac('email'), // non-derivable in practice → ~0
|
|
116
|
+
f_payment: frac('payment'), // non-derivable in practice → ~0
|
|
117
|
+
f_secret: frac('secret'), // non-derivable in practice → ~0
|
|
118
|
+
f_search: frac('search'),
|
|
119
|
+
f_memory: frac('memory'), // non-derivable in practice → ~0
|
|
120
|
+
f_file: frac('file'),
|
|
121
|
+
// event-type fractions (over all events)
|
|
122
|
+
f_handoff: handoff,
|
|
123
|
+
f_user_msg: userMsg,
|
|
124
|
+
// discriminator flags (best-effort; only flag_deploy has any behavioural
|
|
125
|
+
// signal — and only if the agent literally names a deploy tool).
|
|
126
|
+
flag_deploy: deployUses > 0 ? 1 : 0,
|
|
127
|
+
flag_internal_sys: 0, // no behavioural signal in logs
|
|
128
|
+
flag_on_behalf: 0, // no behavioural signal in logs
|
|
129
|
+
// aux ratios
|
|
130
|
+
aux_autonomy: auxAutonomy, // heuristic (HITL-frequency)
|
|
131
|
+
aux_untrusted: 0, // no honest source in logs
|
|
132
|
+
aux_sensitive: 0, // no honest source in logs
|
|
133
|
+
// window size
|
|
134
|
+
n_events: nEvents,
|
|
135
|
+
};
|
|
136
|
+
}
|