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 +2 -2
- package/package.json +1 -1
- package/src/anonymizer.js +29 -2
- package/src/logger.js +7 -0
- package/src/sources/anthropic-managed.js +22 -0
- package/src/sources/contract.js +13 -0
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),
|
|
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.
|
|
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,
|
package/src/sources/contract.js
CHANGED
|
@@ -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'];
|