watchmyagents 1.0.1 → 1.0.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-signals` output), the agent's **`classification`** when the daemon has it (`{agent_type, confidence, stage}` — anonymized metadata, never raw content), **plus the routing identifiers**: `provider` (e.g., `"anthropic-managed"` — added in v1.0 for the multi-framework SDK), `native_agent_id` (the canonical provider-agnostic field), `anthropic_agent_id` (kept for backwards compat with existing Fortress instances; will be dropped once Fortress migrates), `parent_agent_id` (`null` for root agents — populated for sub-agents detected via OpenAI Agents handoffs, CrewAI manager mode, Hermes Agent `spawn_subagent`, LangGraph sub-graphs), `composition_pattern` (`"solo" | "hierarchy" | "graph" | "peer"` — defaults to `"solo"` for Anthropic until thread-message detection lands), `enforcement_mode` (`"sync_confirm" | "sync_interrupt" | "detect_only"` — the strongest enforcement capability the Source provides; Fortress greys out Shield UI for `detect_only` agents to prevent UI/runtime mismatch), 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.
158
+ **What is sent:** the anonymized signals payload (counts, latencies, salted IoC hashes, sequences — same as `wma-signals` output), the agent's **`classification`** when the daemon has it (`{agent_type, confidence, stage}` — anonymized metadata, never raw content), **plus the routing identifiers**: `provider` (e.g., `"anthropic-managed"` — added in v1.0 for the multi-framework SDK), `native_agent_id` (the canonical provider-agnostic field), `anthropic_agent_id` (kept for backwards compat with existing Fortress instances; will be dropped once Fortress migrates), `parent_agent_id` (`null` for root agents — populated for sub-agents detected via OpenAI Agents handoffs, CrewAI manager mode, Hermes Agent `spawn_subagent`, LangGraph sub-graphs), `composition_pattern` (`"solo" | "hierarchy" | "graph" | "peer"` — defaults to `"solo"` for Anthropic until thread-message detection lands), `enforcement_mode` (`"sync_confirm" | "sync_interrupt" | "detect_only"` — the strongest enforcement capability the Source provides; Fortress greys out Shield UI for `detect_only` agents to prevent UI/runtime mismatch), **`session_ids[]`** (opaque vendor session tokens — e.g. Anthropic `sess_01XaNB…` — added in v1.0.2 so an operator looking at a Shield decision in Fortress can `grep` the local NDJSON immediately for full raw context ; non-secret but sensitive, see [docs/CONTAINMENT.md](docs/CONTAINMENT.md#routing--forensic-metadata--what-can-cross-to-fortress) for Fortress-side guardrails), 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.
@@ -247,7 +247,7 @@ WatchMyAgents is built so that **your prompts and outputs never have to leave yo
247
247
  |---|---|
248
248
  | **Your machine** (`./watchmyagents-logs/`) | Full NDJSON with all prompts, tool inputs, agent outputs. `chmod 600` on every file. |
249
249
  | **Anthropic API** | Where the agent runs. WMA pulls events via the public REST API only. |
250
- | **WMA Fortress** (opt-in, only with `--upload` / `wma-upload-fortress` / `wma-shield --policies-source fortress`) | The **anonymized signals** payload (counts, timings, salted hashes, sequences) + routing identifiers: `provider` (e.g. `"anthropic-managed"`), `native_agent_id`, `anthropic_agent_id` (legacy alias), and `display_name` (defaults to the **human agent name** for dashboard UX — pass `--no-send-agent-names` to opt out and send only the agent id). Shield enforcement **decisions** (hashed session/event/input fingerprints — never raw values). **Never** raw prompts, URLs, commands, or outputs. |
250
+ | **WMA Fortress** (opt-in, only with `--upload` / `wma-upload-fortress` / `wma-shield --policies-source fortress`) | The **anonymized signals** payload (counts, timings, salted hashes, sequences) + routing identifiers: `provider` (e.g. `"anthropic-managed"`), `native_agent_id`, `anthropic_agent_id` (legacy alias), `display_name` (defaults to the **human agent name** for dashboard UX — pass `--no-send-agent-names` to opt out and send only the agent id), and **`session_ids[]`** (opaque vendor session tokens, v1.0.2+, used by operators to grep their LOCAL NDJSON for full context after a Shield decision; non-secret but sensitive — Fortress applies RBAC, UI masking with reveal+audit, and retention limits, see [docs/CONTAINMENT.md](docs/CONTAINMENT.md)). Shield enforcement **decisions** (hashed session/event/input fingerprints — never raw values). **Never** raw prompts, URLs, commands, or outputs. |
251
251
 
252
252
  This is the "local-first" guarantee: **raw payloads never leave your machine.** Cloud upload is opt-in and carries only anonymized metadata + the agent id/name needed to route it.
253
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "1.0.1",
3
+ "version": "1.0.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/src/anonymizer.js CHANGED
@@ -18,10 +18,18 @@
18
18
  // - output.content (agent text)
19
19
  // - raw URLs / commands / queries
20
20
  // - error messages
21
- // - readable session_id (hashed)
22
- // - readable agent_id (hashed)
23
21
  // - PII of any kind
24
22
  //
23
+ // Forensic routing metadata that DOES cross to Fortress (opaque tokens,
24
+ // no semantic content, same sensitivity class as agent_id):
25
+ // - session_ids[] — opaque vendor session ids (e.g. Anthropic
26
+ // `sess_01XaNB…`). Sent so the operator looking
27
+ // at a Shield decision in Fortress can grep the
28
+ // LOCAL NDJSON for full raw context.
29
+ // → see docs/CONTAINMENT.md "Routing & forensic
30
+ // metadata" + the Fortress-side guardrails
31
+ // (RBAC, UI masking, audit log, retention).
32
+ //
25
33
  // This is the single bottleneck between Watch (local) and Fortress (cloud).
26
34
  // Every byte that crosses to the cloud passes through this module.
27
35
 
@@ -134,6 +142,13 @@ export class SignalsAggregator {
134
142
  this.entryCount = 0;
135
143
  this._prevActionType = null;
136
144
  this._prevSessionId = null;
145
+ // v1.0.2 F-6b — opaque session ids active in this window. Shipped to
146
+ // Fortress in the payload as `session_ids[]` so an operator looking at
147
+ // a Shield decision in the dashboard can grep their LOCAL NDJSON by
148
+ // session_id immediately (forensics short-circuit). The Anthropic
149
+ // session_id is a non-semantic token like `sess_01XaNB…` — same
150
+ // sensitivity class as `agent_id`, which we already transmit.
151
+ this.seenSessions = new Set(); // unique session_ids
137
152
  }
138
153
 
139
154
  add(entry) {
@@ -147,6 +162,13 @@ export class SignalsAggregator {
147
162
  if (!this.windowEnd || ts > this.windowEnd) this.windowEnd = ts;
148
163
  }
149
164
 
165
+ // F-6b — collect every distinct session_id encountered in the window.
166
+ // Stays opaque (no string transformation), bounded by the natural
167
+ // number of sessions in the window.
168
+ if (typeof entry.session_id === 'string' && entry.session_id.length > 0) {
169
+ this.seenSessions.add(entry.session_id);
170
+ }
171
+
150
172
  // Counts
151
173
  const at = entry.action_type || 'unknown';
152
174
  this.counts[at] = (this.counts[at] || 0) + 1;
@@ -233,6 +255,11 @@ export class SignalsAggregator {
233
255
  sequences_top10: sequencesTop,
234
256
  stop_reasons: this.stopReasons,
235
257
  tokens_total: this.tokensTotal,
258
+ // F-6c — opaque session ids active in this window, sorted for
259
+ // determinism. Operator forensic chain:
260
+ // Fortress decision → window_start/end + session_ids → grep
261
+ // the local NDJSON of the affected agent → full raw context.
262
+ session_ids: [...this.seenSessions].sort(),
236
263
  },
237
264
  _meta: {
238
265
  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'];