watchmyagents 1.4.1 → 1.4.4

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.
@@ -101,6 +101,23 @@ export const ACTION_TYPES = Object.freeze({
101
101
  STATE_TRANSITION: 'state_transition',
102
102
  });
103
103
 
104
+ // v1.4.2 F-38 (P0 audit) — the three tool-INVOCATION action_types form one
105
+ // security-equivalent family. WHICH surface a tool call came through
106
+ // (provider built-in vs MCP server vs customer-wired custom tool) is an
107
+ // adapter implementation detail, not a security distinction: all three are
108
+ // "the agent invoked a tool." A deny/allowlist policy keyed on the generic
109
+ // `tool_use` MUST catch all three, or it silently misses MCP + custom tool
110
+ // calls — exactly where an attacker pivots. The policy matcher
111
+ // (src/shield/policy.js) expands a generic `tool_use` target to this family;
112
+ // matching a SPECIFIC member (mcp_tool_use / custom_tool_use) stays exact so
113
+ // an operator can still target one surface when they mean to.
114
+ // SignalsAggregator (src/anonymizer.js) uses the same set for IoC capture.
115
+ export const TOOL_USE_FAMILY = Object.freeze([
116
+ ACTION_TYPES.TOOL_USE,
117
+ ACTION_TYPES.MCP_TOOL_USE,
118
+ ACTION_TYPES.CUSTOM_TOOL_USE,
119
+ ]);
120
+
104
121
  export const STATUS_VALUES = Object.freeze({
105
122
  OK: 'ok',
106
123
  ERROR: 'error',
@@ -549,10 +549,26 @@ export function wmaToolInputGuardrail(options = {}) {
549
549
  try {
550
550
  const rs = await ensureRuleset();
551
551
 
552
+ // v1.4.2 F-42 (P1 audit): read the tool name DEFENSIVELY across the
553
+ // shapes @openai/agents may use for the guardrail's toolCall object.
554
+ // The adapter already knows the id lives under a non-obvious key
555
+ // (makeEventId reads callId||id), so the name may likewise sit under
556
+ // .name, .toolName, or .function.name depending on SDK version/tool
557
+ // kind. If we only read .name and the SDK puts it elsewhere, tool_name
558
+ // is null and EVERY tool_name-keyed deny rule silently misses — a
559
+ // total enforcement bypass. Coalesce all known shapes; null only if
560
+ // none are present (then tool_name-keyed rules correctly no-match,
561
+ // and the operator can still deny on action_type alone).
562
+ const toolName = toolCall?.name
563
+ ?? toolCall?.toolName
564
+ ?? toolCall?.function?.name
565
+ ?? data?.tool?.name
566
+ ?? null;
567
+
552
568
  // 1. Normalize the about-to-fire tool_use event.
553
569
  const event = normalizeToolStart({
554
570
  agent,
555
- tool: { name: toolCall?.name },
571
+ tool: { name: toolName },
556
572
  toolCall,
557
573
  sessionId,
558
574
  teamId: getTeamId(),
package/src/tokens.js CHANGED
@@ -4,14 +4,26 @@
4
4
  // token counts (split + by-model) are tracked.
5
5
  export const DEFAULT_PRICING = {};
6
6
 
7
+ // v1.4.2 F-49 (P2 audit): coerce a token/usage value to a safe non-negative
8
+ // integer. The old `x || 0` is a FALSY guard, not a numeric one — it lets
9
+ // negatives through (`-5 || 0` === -5) and a NaN propagates through later
10
+ // arithmetic to poison totals (`NaN + n` → NaN → `|| null` → null), silently
11
+ // ZEROING a real LLM call's tokens. A malformed or adversarial usage payload
12
+ // could thus under-report the security-relevant consumption signal (a runaway
13
+ // / abused agent) or drive aggregate token/cost negative. Anything not a
14
+ // finite non-negative number becomes 0.
15
+ export function safeNonNegInt(x) {
16
+ return Number.isFinite(x) && x >= 0 ? Math.trunc(x) : 0;
17
+ }
18
+
7
19
  export function estimateCost(model, t, pricing) {
8
20
  if (!model) return null;
9
21
  const p = (pricing && pricing[model]) || DEFAULT_PRICING[model];
10
22
  if (!p) return null;
11
- const inT = t.input_tokens || 0;
12
- const outT = t.output_tokens || 0;
13
- const cr = t.cache_read_tokens || 0;
14
- const cw = t.cache_creation_tokens || 0;
23
+ const inT = safeNonNegInt(t.input_tokens);
24
+ const outT = safeNonNegInt(t.output_tokens);
25
+ const cr = safeNonNegInt(t.cache_read_tokens);
26
+ const cw = safeNonNegInt(t.cache_creation_tokens);
15
27
  const cost = ((inT * (p.input || 0)) + (outT * (p.output || 0)) +
16
28
  (cr * (p.cache_read || 0)) + (cw * (p.cache_write || 0))) / 1_000_000;
17
29
  return Math.round(cost * 1_000_000) / 1_000_000;
@@ -38,13 +50,16 @@ export class TokenTracker {
38
50
  if (entry.action_type === 'session_end') return;
39
51
 
40
52
  const t = {
41
- input: entry.input_tokens || 0,
42
- output: entry.output_tokens || 0,
43
- cache_read: entry.cache_read_tokens || 0,
44
- cache_creation: entry.cache_creation_tokens || 0,
53
+ input: safeNonNegInt(entry.input_tokens),
54
+ output: safeNonNegInt(entry.output_tokens),
55
+ cache_read: safeNonNegInt(entry.cache_read_tokens),
56
+ cache_creation: safeNonNegInt(entry.cache_creation_tokens),
45
57
  };
46
- const sum = entry.tokens_used || (t.input + t.output + t.cache_read + t.cache_creation);
47
- const cost = entry.cost_usd || 0;
58
+ // F-49: a provided tokens_used is sanitized too; 0/missing falls back to
59
+ // the (sanitized) component sum.
60
+ const provided = safeNonNegInt(entry.tokens_used);
61
+ const sum = provided || (t.input + t.output + t.cache_read + t.cache_creation);
62
+ const cost = Number.isFinite(entry.cost_usd) && entry.cost_usd >= 0 ? entry.cost_usd : 0;
48
63
 
49
64
  this.total.input += t.input;
50
65
  this.total.output += t.output;
@@ -0,0 +1,65 @@
1
+ // v1.4.3 F-51 (P2 audit) — bounded dedup state for the long-running watch loop.
2
+ //
3
+ // The watch daemon dedupes events by their stable Anthropic event id so a
4
+ // re-fetch of an already-captured session doesn't double-write the NDJSON.
5
+ // Pre-F-51 every id ever seen lived in a single Set that was only ever added
6
+ // to — on a 24/7 daemon at high event volume that Set grows without bound
7
+ // (hundreds of MB within days), and the daemon eventually OOM-kills and stops
8
+ // collecting (a silent monitoring gap).
9
+ //
10
+ // SeenTracker bounds runtime growth WITHOUT weakening dedup:
11
+ // - Preloaded ids (read from on-disk NDJSON at startup) sit in a static set,
12
+ // loaded once. Bounded by what the operator keeps on disk.
13
+ // - Runtime-discovered ids are tracked PER SESSION. When a session
14
+ // terminates it is never re-fetched, so its id set is dropped. Live memory
15
+ // is therefore bounded by ACTIVE sessions, not lifetime event volume.
16
+ //
17
+ // Correctness: Anthropic event ids are globally unique and an event belongs to
18
+ // exactly one session, so checking the preloaded set + that session's set is
19
+ // equivalent to the old global-set membership test (no missed dedup, no
20
+ // missed events).
21
+ export class SeenTracker {
22
+ constructor(preloadedIds = []) {
23
+ this._preloaded = new Set(preloadedIds);
24
+ this._bySession = new Map(); // sessionId -> Set<eventId>
25
+ }
26
+
27
+ // v1.4.4 F-53: fold an agent's on-disk history into the static preloaded
28
+ // set. Called lazily the first time an agent is seen (including agents that
29
+ // --all-agents discovers AFTER startup) so their already-captured NDJSON ids
30
+ // are deduped against — otherwise a late-appearing agent with existing logs
31
+ // would re-append and re-upload events it already has.
32
+ addPreloaded(id) {
33
+ if (id) this._preloaded.add(id);
34
+ }
35
+
36
+ has(sessionId, eventId) {
37
+ if (this._preloaded.has(eventId)) return true;
38
+ const s = this._bySession.get(sessionId);
39
+ return s ? s.has(eventId) : false;
40
+ }
41
+
42
+ add(sessionId, eventId) {
43
+ let s = this._bySession.get(sessionId);
44
+ if (!s) { s = new Set(); this._bySession.set(sessionId, s); }
45
+ s.add(eventId);
46
+ }
47
+
48
+ // Drop a terminated session's runtime id set — it will never be re-fetched,
49
+ // so its ids no longer need to be remembered for dedup.
50
+ forgetSession(sessionId) {
51
+ this._bySession.delete(sessionId);
52
+ }
53
+
54
+ // Total tracked ids (preloaded + all live per-session sets). For observability.
55
+ get size() {
56
+ let n = this._preloaded.size;
57
+ for (const s of this._bySession.values()) n += s.size;
58
+ return n;
59
+ }
60
+
61
+ // Number of sessions currently holding a runtime id set.
62
+ get sessionCount() {
63
+ return this._bySession.size;
64
+ }
65
+ }