watchmyagents 1.0.1 → 1.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "1.0.1",
3
+ "version": "1.0.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/src/anonymizer.js CHANGED
@@ -134,6 +134,13 @@ export class SignalsAggregator {
134
134
  this.entryCount = 0;
135
135
  this._prevActionType = null;
136
136
  this._prevSessionId = null;
137
+ // v1.0.2 F-6b — opaque session ids active in this window. Shipped to
138
+ // Fortress in the payload as `session_ids[]` so an operator looking at
139
+ // a Shield decision in the dashboard can grep their LOCAL NDJSON by
140
+ // session_id immediately (forensics short-circuit). The Anthropic
141
+ // session_id is a non-semantic token like `sess_01XaNB…` — same
142
+ // sensitivity class as `agent_id`, which we already transmit.
143
+ this.seenSessions = new Set(); // unique session_ids
137
144
  }
138
145
 
139
146
  add(entry) {
@@ -147,6 +154,13 @@ export class SignalsAggregator {
147
154
  if (!this.windowEnd || ts > this.windowEnd) this.windowEnd = ts;
148
155
  }
149
156
 
157
+ // F-6b — collect every distinct session_id encountered in the window.
158
+ // Stays opaque (no string transformation), bounded by the natural
159
+ // number of sessions in the window.
160
+ if (typeof entry.session_id === 'string' && entry.session_id.length > 0) {
161
+ this.seenSessions.add(entry.session_id);
162
+ }
163
+
150
164
  // Counts
151
165
  const at = entry.action_type || 'unknown';
152
166
  this.counts[at] = (this.counts[at] || 0) + 1;
@@ -233,6 +247,11 @@ export class SignalsAggregator {
233
247
  sequences_top10: sequencesTop,
234
248
  stop_reasons: this.stopReasons,
235
249
  tokens_total: this.tokensTotal,
250
+ // F-6c — opaque session ids active in this window, sorted for
251
+ // determinism. Operator forensic chain:
252
+ // Fortress decision → window_start/end + session_ids → grep
253
+ // the local NDJSON of the affected agent → full raw context.
254
+ session_ids: [...this.seenSessions].sort(),
236
255
  },
237
256
  _meta: {
238
257
  entries_processed: this.entryCount,
package/src/logger.js CHANGED
@@ -13,6 +13,8 @@ import { assertSafePathSegment } from './validate.js';
13
13
  const EXPORT_FIELDS = [
14
14
  'id', 'agent_id', 'parent_agent_id', 'composition_pattern',
15
15
  'provider', 'timestamp', 'action_type',
16
+ // v1.0.2 F-6a — Anthropic-style sub-agent discriminators preserved locally
17
+ 'session_thread_id', 'agent_name',
16
18
  'tool_name', 'duration_ms', 'tokens_used',
17
19
  'input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_creation_tokens',
18
20
  'cost_usd', 'model',
@@ -60,6 +62,11 @@ export class Logger {
60
62
  // populates these on the event, and the Logger threads them through.
61
63
  parent_agent_id: e.parent_agent_id ?? null,
62
64
  composition_pattern: e.composition_pattern || 'solo',
65
+ // v1.0.2 F-6a: Anthropic-style discriminators preserved LOCAL ONLY
66
+ // (never sent raw to Fortress — SignalsAggregator derives the
67
+ // aggregated session_ids list from these at finalize time).
68
+ session_thread_id: e.session_thread_id ?? null,
69
+ agent_name: e.agent_name ?? null,
63
70
  provider: e.provider || e.framework || 'generic',
64
71
  timestamp: e.timestamp || new Date().toISOString(),
65
72
  action_type: e.action_type || 'tool_call',
@@ -185,6 +185,13 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
185
185
  if (!RELEVANT.has(ev.type)) continue;
186
186
  const type = ev.type;
187
187
  const ts = ev.processed_at || ev.created_at || new Date().toISOString();
188
+ // v1.0.2 F-6a: capture Anthropic's own discriminators on EVERY event,
189
+ // not just thread_message_*. session_thread_id + agent_name are how
190
+ // the vendor itself tells parent activity from sub-agent activity.
191
+ // Preserved LOCALLY (NDJSON) only — never sent raw to Fortress.
192
+ const session_thread_id = ev.session_thread_id ?? null;
193
+ const agent_name = ev.agent_name ?? null;
194
+ const subAgentMeta = { session_thread_id, agent_name };
188
195
  const tsMillis = tsMs(ev);
189
196
 
190
197
  if (type === 'span.model_request_start') {
@@ -201,6 +208,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
201
208
  const cw = u.cache_creation_input_tokens || 0;
202
209
  yield {
203
210
  ...base,
211
+ ...subAgentMeta,
204
212
  id: ev.id,
205
213
  action_type: 'llm_call',
206
214
  tool_name: null,
@@ -220,6 +228,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
220
228
  if (type === 'user.message') {
221
229
  yield {
222
230
  ...base,
231
+ ...subAgentMeta,
223
232
  id: ev.id,
224
233
  action_type: 'user_message',
225
234
  tool_name: null,
@@ -234,6 +243,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
234
243
  if (type === 'user.interrupt') {
235
244
  yield {
236
245
  ...base,
246
+ ...subAgentMeta,
237
247
  id: ev.id,
238
248
  action_type: 'user_interrupt',
239
249
  tool_name: null,
@@ -249,6 +259,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
249
259
  const denied = ev.result === 'deny';
250
260
  yield {
251
261
  ...base,
262
+ ...subAgentMeta,
252
263
  id: ev.id,
253
264
  action_type: 'tool_confirmation',
254
265
  tool_name: null,
@@ -265,6 +276,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
265
276
  if (type === 'user.custom_tool_result') {
266
277
  yield {
267
278
  ...base,
279
+ ...subAgentMeta,
268
280
  id: ev.id,
269
281
  action_type: 'custom_tool_result',
270
282
  tool_name: null,
@@ -280,6 +292,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
280
292
  if (type === 'agent.message') {
281
293
  yield {
282
294
  ...base,
295
+ ...subAgentMeta,
283
296
  id: ev.id,
284
297
  action_type: 'message',
285
298
  tool_name: null,
@@ -294,6 +307,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
294
307
  if (type === 'agent.thinking') {
295
308
  yield {
296
309
  ...base,
310
+ ...subAgentMeta,
297
311
  id: ev.id,
298
312
  action_type: 'thinking',
299
313
  tool_name: null,
@@ -321,6 +335,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
321
335
  const isError = ev.is_error === true;
322
336
  yield {
323
337
  ...base,
338
+ ...subAgentMeta,
324
339
  id: ev.id,
325
340
  action_type: start?.isMcp ? 'mcp_tool_use' : 'tool_use',
326
341
  tool_name: start?.name || 'unknown',
@@ -337,6 +352,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
337
352
  if (type === 'agent.custom_tool_use') {
338
353
  yield {
339
354
  ...base,
355
+ ...subAgentMeta,
340
356
  id: ev.id,
341
357
  action_type: 'custom_tool_use',
342
358
  tool_name: ev.name || 'unknown',
@@ -351,6 +367,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
351
367
  if (type === 'agent.thread_context_compacted') {
352
368
  yield {
353
369
  ...base,
370
+ ...subAgentMeta,
354
371
  id: ev.id,
355
372
  action_type: 'context_compacted',
356
373
  tool_name: null,
@@ -370,6 +387,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
370
387
  const direction = type.endsWith('_sent') ? 'sent' : 'received';
371
388
  yield {
372
389
  ...base,
390
+ ...subAgentMeta,
373
391
  id: ev.id,
374
392
  action_type: `thread_message_${direction}`,
375
393
  tool_name: null,
@@ -391,6 +409,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
391
409
  const { id: _id, type: _type, processed_at: _pa, created_at: _ca, ...changes } = ev;
392
410
  yield {
393
411
  ...base,
412
+ ...subAgentMeta,
394
413
  id: ev.id,
395
414
  action_type: 'config_change',
396
415
  tool_name: null,
@@ -405,6 +424,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
405
424
  if (type === 'session.thread_created') {
406
425
  yield {
407
426
  ...base,
427
+ ...subAgentMeta,
408
428
  id: ev.id,
409
429
  action_type: 'thread_created',
410
430
  tool_name: null,
@@ -422,6 +442,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
422
442
  if (type === 'session.error') {
423
443
  yield {
424
444
  ...base,
445
+ ...subAgentMeta,
425
446
  id: ev.id,
426
447
  action_type: 'session_error',
427
448
  tool_name: null,
@@ -443,6 +464,7 @@ export async function* fetchSessionEntries({ apiKey, agentId, sessionId, model }
443
464
  const fatal = state === 'terminated';
444
465
  yield {
445
466
  ...base,
467
+ ...subAgentMeta,
446
468
  id: ev.id,
447
469
  action_type: 'state_transition',
448
470
  tool_name: null,
@@ -127,6 +127,19 @@ export const PROVIDERS = Object.freeze({
127
127
  // * SUB-AGENT FIELDS (PR-C — see WMAAction.parent_agent_id):
128
128
  // * @property {string|null} parent_agent_id Null for root agents
129
129
  // * @property {string|null} composition_pattern From COMPOSITION_PATTERNS
130
+ // *
131
+ // * MULTI-AGENT DISCRIMINATORS (v1.0.2 F-6a — preserved LOCALLY only,
132
+ // * never sent raw to Fortress; the SignalsAggregator derives the
133
+ // * aggregated session_ids list from them at finalize time):
134
+ // * @property {string|null} session_thread_id The thread the event happened in.
135
+ // * For frameworks where one session can
136
+ // * host multiple threads/sub-agents
137
+ // * (Anthropic Task tool, future similar
138
+ // * designs), this is how the vendor
139
+ // * itself discriminates "parent vs sub".
140
+ // * @property {string|null} agent_name The human-named emitter of this event
141
+ // * (the parent agent OR a sub-agent
142
+ // * running inside the parent's session).
130
143
  // */
131
144
 
132
145
  const REQUIRED_FIELDS = ['id', 'provider', 'agent_id', 'session_id', 'action_type', 'timestamp', 'status'];