watchmyagents 0.9.1 → 0.9.3

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/README.md CHANGED
@@ -155,7 +155,7 @@ wma-upload-fortress --agent-id agent_01ABC... [--display-name "My agent"]
155
155
  wma-upload-fortress --agent-id agent_xxx --dry-run
156
156
  ```
157
157
 
158
- **What is sent:** the anonymized signals payload (counts, latencies, salted IoC hashes, sequences — same as `wma-anonymize` output) **plus two routing identifiers**: your `anthropic_agent_id` and a `display_name`. The agent id is required so Fortress can associate signals with the right agent; `display_name` defaults to the agent id and only carries the human-readable agent name if you opt in (`wma-fetch --watch --upload --send-agent-names`).
158
+ **What is sent:** the anonymized signals payload (counts, latencies, salted IoC hashes, sequences — same as `wma-anonymize` output), the agent's **`classification`** when the daemon has it (`{agent_type, confidence, stage}` — anonymized metadata, never raw content), **plus two routing identifiers**: your `anthropic_agent_id` and a `display_name`. The agent id is required so Fortress can associate signals with the right agent; `display_name` defaults to the **human-readable agent name** (sanitized to strip control chars) for UX in the dashboard pass `--no-send-agent-names` to keep it pseudonymized (sends the agent id instead) if your agent names themselves carry sensitive client/project info.
159
159
  **What is NOT sent:** raw prompts, raw URLs/commands/queries, raw agent responses, raw error messages. All payload content stays on your machine.
160
160
 
161
161
  The endpoint auto-registers the agent on the first upload if it doesn't exist in Fortress yet — no manual onboarding needed for new agents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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
- // Action types that represent a TOOL invocation (the denominator for f_* tool
44
- // fractions). Confirmed produced by src/sources/anthropic-managed.js.
45
- const TOOL_ACTIONS = new Set(['tool_use', 'mcp_tool_use', 'custom_tool_use']);
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
- async function uploadSignals(uploadCtx, agentId, displayName, entries) {
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
- const resp = await uploadSignals(uploadCtx, ag.agentId, sendNames ? ag.displayName : ag.agentId, fresh);
278
- if (resp?.signal_id) info(` ↑ signals uploaded (signal_id ${resp.signal_id})`);
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
 
@@ -342,7 +365,12 @@ async function main() {
342
365
  // Discovery window for NEW sessions (default 7d, configurable). Sessions we
343
366
  // already track are re-fetched regardless of age, so long-lived ones don't drop.
344
367
  const windowMs = parseDurationMs(args['discovery-since'], 7 * 24 * 3600_000);
345
- const sendNames = !!args['send-agent-names'];
368
+ // display_name on the Fortress payload: defaults to the human agent name
369
+ // (UX-friendly — operators identify agents by name in the dashboard). The
370
+ // name is sanitized via cleanLabel() so log/payload injection is impossible.
371
+ // Use --no-send-agent-names to opt OUT (sends the agent_id instead) for
372
+ // setups where the agent name itself is considered sensitive metadata.
373
+ const sendNames = args['no-send-agent-names'] !== true;
346
374
 
347
375
  let resolveAgents;
348
376
  if (allAgents) {
@@ -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
+ }