watchmyagents 0.2.0 → 0.3.0

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,20 +1,8 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "0.2.0",
4
- "description": "Security observability for AI agents — local-first NDJSON capture of every agent action (tool calls, prompts, state transitions, errors). Built for security audits, not just token counting.",
3
+ "version": "0.3.0",
4
+ "description": "Security observability + real-time policy enforcement for AI agents — local-first NDJSON capture of every agent action (tool calls, prompts, state transitions, errors) plus the Shield CLI that blocks policy violations live on Anthropic Managed Agents.",
5
5
  "type": "module",
6
- "main": "./src/index.cjs",
7
- "module": "./src/index.js",
8
- "exports": {
9
- ".": {
10
- "import": "./src/index.js",
11
- "require": "./src/index.cjs"
12
- },
13
- "./adapters/claude": "./src/adapters/claude.js",
14
- "./adapters/openai": "./src/adapters/openai.js",
15
- "./adapters/langchain": "./src/adapters/langchain.js",
16
- "./adapters/generic": "./src/adapters/generic.js"
17
- },
18
6
  "files": [
19
7
  "src/",
20
8
  "scripts/inspect.js",
@@ -32,15 +20,14 @@
32
20
  "scripts": {
33
21
  "inspect": "node scripts/inspect.js",
34
22
  "fetch": "node scripts/fetch-anthropic.js",
35
- "shield": "node scripts/shield.js",
36
- "example": "node examples/claude-agent/index.js"
23
+ "shield": "node scripts/shield.js"
37
24
  },
38
25
  "engines": {
39
26
  "node": ">=18.0.0"
40
27
  },
41
28
  "dependencies": {},
42
29
  "devDependencies": {
43
- "@anthropic-ai/sdk": "latest"
30
+ "@anthropic-ai/sdk": "^0.42.0"
44
31
  },
45
32
  "keywords": [
46
33
  "ai",
@@ -54,7 +41,9 @@
54
41
  "claude",
55
42
  "managed-agents",
56
43
  "audit",
57
- "ndjson"
44
+ "ndjson",
45
+ "policy-enforcement",
46
+ "shield"
58
47
  ],
59
48
  "author": "MinedorFBM <minedor@watchmyagents.com>",
60
49
  "license": "MIT",
package/scripts/shield.js CHANGED
@@ -118,6 +118,9 @@ async function runSessionWorker({ sessionId, ctx }) {
118
118
  sinfo(sessionId, `attached (${mode} mode)`);
119
119
 
120
120
  let processed = 0, enforced = 0, sessionInterrupted = false;
121
+ // Cache is only needed for tool_confirmation mode (lookup by event_id when
122
+ // requires_action fires). Interrupt mode evaluates synchronously and never
123
+ // reads the cache, so caching there would just leak memory on long sessions.
121
124
  const toolUseCache = new Map();
122
125
 
123
126
  try {
@@ -131,7 +134,7 @@ async function runSessionWorker({ sessionId, ctx }) {
131
134
 
132
135
  // ── INTERRUPT MODE ──────────────────────────────────────────────
133
136
  if (mode === 'interrupt' && CACHEABLE_TOOL_TYPES.has(rawEvent.type)) {
134
- toolUseCache.set(rawEvent.id, rawEvent);
137
+ // No caching in interrupt mode — react synchronously, free memory.
135
138
  const normalized = normalizeForPolicy(rawEvent);
136
139
  const t0 = Date.now();
137
140
  const result = evaluate(normalized, ruleset);
@@ -1,46 +0,0 @@
1
- import { WatchMyAgents } from '../collector.js';
2
-
3
- export function createClaudeMonitor(opts = {}) {
4
- const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'claude' });
5
-
6
- return {
7
- wma,
8
- wrap(client) {
9
- const m = client?.messages;
10
- if (!m?.create) return client;
11
- const orig = m.create.bind(m);
12
- m.create = async (params) => {
13
- const start = Date.now();
14
- let status = 'ok', error = null, res;
15
- try { res = await orig(params); return res; }
16
- catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
17
- finally {
18
- const u = res?.usage || {};
19
- const inT = u.input_tokens || 0;
20
- const outT = u.output_tokens || 0;
21
- const cr = u.cache_read_input_tokens || 0;
22
- const cw = u.cache_creation_input_tokens || 0;
23
- const toolUses = Array.isArray(res?.content)
24
- ? res.content.filter(b => b?.type === 'tool_use').map(b => b.name) : [];
25
- await wma.logAction({
26
- framework: 'claude', action_type: 'llm_call',
27
- tool_name: params?.model || 'messages.create',
28
- model: params?.model || null,
29
- duration_ms: Date.now() - start,
30
- input_tokens: inT || null,
31
- output_tokens: outT || null,
32
- cache_read_tokens: cr || null,
33
- cache_creation_tokens: cw || null,
34
- tokens_used: (inT + outT + cr + cw) || null,
35
- status, error,
36
- input: { model: params?.model, message_count: params?.messages?.length, tool_count: params?.tools?.length || 0 },
37
- output: { stop_reason: res?.stop_reason || null, tool_uses: toolUses },
38
- });
39
- }
40
- };
41
- return client;
42
- },
43
- logToolUse: (name, input, output, duration_ms) =>
44
- wma.logAction({ framework: 'claude', action_type: 'tool_use', tool_name: name, duration_ms: duration_ms ?? null, status: 'ok', input, output }),
45
- };
46
- }
@@ -1,21 +0,0 @@
1
- import { WatchMyAgents } from '../collector.js';
2
-
3
- export async function watch(toolName, params, fn, meta = {}) {
4
- const wma = WatchMyAgents.getOrCreate();
5
- return wma.watch(toolName, params, fn, { framework: 'generic', ...meta });
6
- }
7
-
8
- export function createGenericMonitor(opts = {}) {
9
- const wma = WatchMyAgents.current() || new WatchMyAgents(opts);
10
- return {
11
- watch: (toolName, params, fn, meta) => wma.watch(toolName, params, fn, { framework: 'generic', ...meta }),
12
- wrap(obj, methodNames) {
13
- const names = methodNames || Object.keys(obj).filter(k => typeof obj[k] === 'function');
14
- const wrapped = {};
15
- for (const name of names) {
16
- wrapped[name] = (...args) => wma.watch(name, args, () => obj[name](...args), { framework: 'generic' });
17
- }
18
- return { ...obj, ...wrapped };
19
- },
20
- };
21
- }
@@ -1,42 +0,0 @@
1
- import { WatchMyAgents } from '../collector.js';
2
-
3
- export function createLangChainHandler(opts = {}) {
4
- const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'langchain' });
5
- const starts = new Map();
6
- const begin = id => starts.set(id, Date.now());
7
- const elapsed = id => { const t = starts.get(id); starts.delete(id); return t ? Date.now() - t : null; };
8
-
9
- return {
10
- name: 'WatchMyAgentsHandler',
11
- handleLLMStart: async (_l, _p, runId) => begin(runId),
12
- handleLLMEnd: async (out, runId) => {
13
- const u = out?.llmOutput?.tokenUsage || {};
14
- const inT = u.promptTokens || 0, outT = u.completionTokens || 0;
15
- return wma.logAction({
16
- framework: 'langchain', action_type: 'llm_call', tool_name: 'llm',
17
- model: out?.llmOutput?.modelName || null,
18
- duration_ms: elapsed(runId),
19
- input_tokens: inT || null, output_tokens: outT || null,
20
- tokens_used: (inT + outT) || null, status: 'ok',
21
- });
22
- },
23
- handleLLMError: async (err, runId) => wma.logAction({
24
- framework: 'langchain', action_type: 'llm_call', tool_name: 'llm',
25
- duration_ms: elapsed(runId), status: 'error', error: err?.message || String(err),
26
- }),
27
- handleToolStart: async (_t, _i, runId) => begin(runId),
28
- handleToolEnd: async (_o, runId) => wma.logAction({
29
- framework: 'langchain', action_type: 'tool_call', tool_name: 'tool',
30
- duration_ms: elapsed(runId), status: 'ok',
31
- }),
32
- handleToolError: async (err, runId) => wma.logAction({
33
- framework: 'langchain', action_type: 'tool_call', tool_name: 'tool',
34
- duration_ms: elapsed(runId), status: 'error', error: err?.message || String(err),
35
- }),
36
- handleChainStart: async (_c, _i, runId) => begin(runId),
37
- handleChainEnd: async (_o, runId) => wma.logAction({
38
- framework: 'langchain', action_type: 'chain', tool_name: 'chain',
39
- duration_ms: elapsed(runId), status: 'ok',
40
- }),
41
- };
42
- }
@@ -1,47 +0,0 @@
1
- import { WatchMyAgents } from '../collector.js';
2
-
3
- export function createOpenAIMonitor(opts = {}) {
4
- const wma = WatchMyAgents.current() || new WatchMyAgents({ ...opts, framework: 'openai' });
5
-
6
- function wrapMethod(obj, method, action_type) {
7
- if (!obj || typeof obj[method] !== 'function') return;
8
- const orig = obj[method].bind(obj);
9
- obj[method] = async (params) => {
10
- const start = Date.now();
11
- let status = 'ok', error = null, res;
12
- try { res = await orig(params); return res; }
13
- catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
14
- finally {
15
- const u = res?.usage || {};
16
- const inT = u.prompt_tokens || u.input_tokens || 0;
17
- const outT = u.completion_tokens || u.output_tokens || 0;
18
- const cr = u.prompt_tokens_details?.cached_tokens || 0;
19
- await wma.logAction({
20
- framework: 'openai', action_type,
21
- tool_name: params?.model || params?.assistant_id || method,
22
- model: params?.model || null,
23
- duration_ms: Date.now() - start,
24
- input_tokens: inT || null,
25
- output_tokens: outT || null,
26
- cache_read_tokens: cr || null,
27
- tokens_used: (inT + outT) || null,
28
- status, error,
29
- input: { model: params?.model, assistant_id: params?.assistant_id },
30
- output: { id: res?.id, status: res?.status },
31
- });
32
- }
33
- };
34
- }
35
-
36
- return {
37
- wma,
38
- wrap(client) {
39
- wrapMethod(client?.chat?.completions, 'create', 'llm_call');
40
- wrapMethod(client?.completions, 'create', 'llm_call');
41
- wrapMethod(client?.beta?.threads?.runs, 'create', 'assistant_run');
42
- wrapMethod(client?.beta?.threads?.runs, 'createAndPoll', 'assistant_run');
43
- wrapMethod(client?.responses, 'create', 'llm_call');
44
- return client;
45
- },
46
- };
47
- }
package/src/anonymizer.js DELETED
@@ -1,48 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
-
3
- const PII_PATTERNS = [
4
- [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '[EMAIL]'],
5
- [/Bearer\s+[A-Za-z0-9\-_\.=]+/gi, '[TOKEN]'],
6
- [/eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+/g, '[TOKEN]'],
7
- [/\b(sk|pk|rk)-[A-Za-z0-9_\-]{16,}\b/g, '[API_KEY]'],
8
- [/\bwma_[A-Za-z0-9_\-]{8,}\b/g, '[API_KEY]'],
9
- [/\b(?:\d[ -]*?){13,19}\b/g, '[CARD]'],
10
- [/\b\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}\b/g, '[PHONE]'],
11
- [/https?:\/\/[^\s"'<>]+/gi, '[URL]'],
12
- [/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[IP]'],
13
- [/\b(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}\b/gi, '[IP]'],
14
- ];
15
-
16
- const HASH_FIELDS = new Set(['user_id', 'session_id', 'agent_id']);
17
-
18
- export function scrubString(str) {
19
- if (typeof str !== 'string') return str;
20
- let out = str;
21
- for (const [re, tag] of PII_PATTERNS) out = out.replace(re, tag);
22
- return out;
23
- }
24
-
25
- export function hashId(value) {
26
- if (value == null) return value;
27
- return 'h_' + createHash('sha256').update(String(value)).digest('hex').slice(0, 24);
28
- }
29
-
30
- export function anonymize(obj) {
31
- if (obj == null) return obj;
32
- if (typeof obj === 'string') return scrubString(obj);
33
- if (typeof obj !== 'object') return obj;
34
- if (Array.isArray(obj)) return obj.map(anonymize);
35
- const out = {};
36
- for (const [k, v] of Object.entries(obj)) {
37
- if (HASH_FIELDS.has(k) && (typeof v === 'string' || typeof v === 'number')) {
38
- out[k] = hashId(v);
39
- } else if (typeof v === 'string') {
40
- out[k] = scrubString(v);
41
- } else if (typeof v === 'object') {
42
- out[k] = anonymize(v);
43
- } else {
44
- out[k] = v;
45
- }
46
- }
47
- return out;
48
- }
package/src/collector.js DELETED
@@ -1,113 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { Logger } from './logger.js';
3
- import { Exporter } from './exporter.js';
4
- import { TokenTracker, estimateCost } from './tokens.js';
5
-
6
- let _instance = null;
7
-
8
- export class WatchMyAgents {
9
- constructor(opts = {}) {
10
- this.apiKey = opts.apiKey || process.env.WMA_API_KEY || null;
11
- this.agentId = opts.agentId || 'default-agent';
12
- this.logDir = opts.logDir || process.env.WMA_LOG_DIR || './watchmyagents-logs';
13
- this.exportUrl = opts.exportUrl || process.env.WMA_EXPORT_URL || null;
14
- this.silent = opts.silent !== false;
15
- this.sessionId = opts.sessionId || randomUUID();
16
- this.framework = opts.framework || 'generic';
17
- this.tokenPricing = opts.tokenPricing || null;
18
- this.tracker = new TokenTracker();
19
- this.logger = new Logger({ logDir: this.logDir, agentId: this.agentId, sessionId: this.sessionId, silent: this.silent });
20
- this.exporter = new Exporter({
21
- apiKey: this.apiKey, exportUrl: this.exportUrl, agentId: this.agentId,
22
- batchInterval: opts.batchInterval ?? 30000, silent: this.silent,
23
- });
24
- this.exporter.start();
25
- _instance = this;
26
- }
27
-
28
- static current() { return _instance; }
29
- static getOrCreate(opts) { return _instance || new WatchMyAgents(opts); }
30
-
31
- summarize(v) {
32
- if (v == null) return null;
33
- const t = typeof v;
34
- if (t === 'string') return { type: 'string', length: v.length };
35
- if (t === 'number' || t === 'boolean') return { type: t };
36
- if (Array.isArray(v)) return { type: 'array', length: v.length };
37
- if (t === 'object') return { type: 'object', keys: Object.keys(v).length };
38
- return { type: t };
39
- }
40
-
41
- _enrichTokens(entry) {
42
- const i = entry.input_tokens, o = entry.output_tokens;
43
- const cr = entry.cache_read_tokens || 0, cw = entry.cache_creation_tokens || 0;
44
- if (entry.tokens_used == null && (i != null || o != null)) {
45
- entry.tokens_used = (i || 0) + (o || 0) + cr + cw;
46
- }
47
- if (entry.cost_usd == null && entry.model) {
48
- entry.cost_usd = estimateCost(entry.model, {
49
- input_tokens: i, output_tokens: o,
50
- cache_read_tokens: cr, cache_creation_tokens: cw,
51
- }, this.tokenPricing);
52
- }
53
- return entry;
54
- }
55
-
56
- async watch(toolName, params, fn, meta = {}) {
57
- const start = Date.now();
58
- const id = randomUUID();
59
- let status = 'ok', error = null, result;
60
- try { result = await fn(); return result; }
61
- catch (e) { status = 'error'; error = e?.message || String(e); throw e; }
62
- finally {
63
- const entry = await this.logger.write(this._enrichTokens({
64
- id, framework: meta.framework || this.framework,
65
- action_type: meta.action_type || 'tool_call',
66
- tool_name: toolName, duration_ms: Date.now() - start,
67
- model: meta.model ?? null,
68
- tokens_used: meta.tokens_used ?? null,
69
- input_tokens: meta.input_tokens ?? null,
70
- output_tokens: meta.output_tokens ?? null,
71
- cache_read_tokens: meta.cache_read_tokens ?? null,
72
- cache_creation_tokens: meta.cache_creation_tokens ?? null,
73
- status, error, input: params, output: this.summarize(result),
74
- }));
75
- this.tracker.record(entry);
76
- this.exporter.enqueue(this.logger.toExportRecord(entry));
77
- }
78
- }
79
-
80
- async logAction(entry) {
81
- const written = await this.logger.write(this._enrichTokens({ ...entry }));
82
- this.tracker.record(written);
83
- this.exporter.enqueue(this.logger.toExportRecord(written));
84
- return written;
85
- }
86
-
87
- tokenStats() { return this.tracker.stats(); }
88
-
89
- async flush() { await this.exporter.flush(); }
90
-
91
- async shutdown() {
92
- const stats = this.tracker.stats().total;
93
- const entry = await this.logger.write({
94
- action_type: 'session_end',
95
- tool_name: null,
96
- framework: this.framework,
97
- status: 'ok',
98
- session_tokens: {
99
- input: stats.input,
100
- output: stats.output,
101
- cache_read: stats.cache_read,
102
- cache_creation: stats.cache_creation,
103
- total: stats.sum,
104
- },
105
- session_cost_usd: stats.cost_usd,
106
- });
107
- this.exporter.enqueue(this.logger.toExportRecord(entry));
108
- this.exporter.stop();
109
- await this.exporter.flush();
110
- }
111
- get logPath() { return this.logger._pathForToday(); }
112
- get actionCount() { return this.logger.count; }
113
- }
package/src/exporter.js DELETED
@@ -1,71 +0,0 @@
1
- import { request } from 'node:https';
2
- import { URL } from 'node:url';
3
- import { createCipheriv, randomBytes, scryptSync } from 'node:crypto';
4
- import { anonymize } from './anonymizer.js';
5
-
6
- const SALT = Buffer.from('watchmyagents.v1.salt', 'utf8');
7
-
8
- export class Exporter {
9
- constructor({ apiKey, exportUrl, agentId, batchInterval = 30000, silent = true }) {
10
- this.apiKey = apiKey;
11
- this.exportUrl = exportUrl;
12
- this.agentId = agentId;
13
- this.silent = silent;
14
- this.queue = [];
15
- this.enabled = !!(apiKey && exportUrl);
16
- this.key = this.enabled ? scryptSync(apiKey, SALT, 32) : null;
17
- this.timer = null;
18
- this.batchInterval = batchInterval;
19
- }
20
-
21
- start() {
22
- if (this.timer || !this.enabled) return;
23
- this.timer = setInterval(() => this.flush().catch(() => {}), this.batchInterval);
24
- if (this.timer.unref) this.timer.unref();
25
- }
26
- stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }
27
- enqueue(record) { if (this.enabled) this.queue.push(anonymize(record)); }
28
-
29
- _encrypt(payload) {
30
- const iv = randomBytes(16);
31
- const c = createCipheriv('aes-256-gcm', this.key, iv);
32
- const data = Buffer.concat([c.update(payload, 'utf8'), c.final()]);
33
- return { iv: iv.toString('base64'), tag: c.getAuthTag().toString('base64'), data: data.toString('base64') };
34
- }
35
-
36
- async flush() {
37
- if (!this.exportUrl || !this.apiKey || this.queue.length === 0) return;
38
- const batch = this.queue.splice(0, this.queue.length);
39
- const body = JSON.stringify({ agent_id: this.agentId, count: batch.length, records: batch });
40
- const payload = JSON.stringify({ v: 1, agent_id: this.agentId, ...this._encrypt(body) });
41
- for (let attempt = 1; attempt <= 3; attempt++) {
42
- try { await this._post(payload); return; }
43
- catch (e) {
44
- if (attempt === 3) {
45
- this.queue.unshift(...batch);
46
- if (!this.silent) process.stderr.write(`[wma] export failed: ${e.message}\n`);
47
- return;
48
- }
49
- await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
50
- }
51
- }
52
- }
53
-
54
- _post(payload) {
55
- return new Promise((resolve, reject) => {
56
- let u; try { u = new URL(this.exportUrl); } catch (e) { return reject(e); }
57
- if (u.protocol !== 'https:') return reject(new Error('HTTPS only'));
58
- const req = request({
59
- host: u.hostname, port: u.port || 443, path: u.pathname + u.search,
60
- method: 'POST', rejectUnauthorized: true,
61
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), 'X-WMA-Key': this.apiKey },
62
- }, res => {
63
- res.resume();
64
- if (res.statusCode >= 200 && res.statusCode < 300) resolve();
65
- else reject(new Error(`HTTP ${res.statusCode}`));
66
- });
67
- req.on('error', reject);
68
- req.write(payload); req.end();
69
- });
70
- }
71
- }
package/src/index.cjs DELETED
@@ -1,36 +0,0 @@
1
- 'use strict';
2
-
3
- // CommonJS entrypoint — re-exports the ESM build via dynamic import.
4
- // All consumers receive the same singleton-backed API.
5
-
6
- const esmPromise = import('./index.js');
7
-
8
- function bind(name) {
9
- return async function (...args) {
10
- const mod = await esmPromise;
11
- return mod[name](...args);
12
- };
13
- }
14
-
15
- class WatchMyAgentsLazy {
16
- constructor(opts) {
17
- this._ready = esmPromise.then(mod => new mod.WatchMyAgents(opts));
18
- }
19
- async watch(...args) { return (await this._ready).watch(...args); }
20
- async logAction(...args) { return (await this._ready).logAction(...args); }
21
- async flush() { return (await this._ready).flush(); }
22
- async shutdown() { return (await this._ready).shutdown(); }
23
- get instance() { return this._ready; }
24
- }
25
-
26
- module.exports = WatchMyAgentsLazy;
27
- module.exports.default = WatchMyAgentsLazy;
28
- module.exports.WatchMyAgents = WatchMyAgentsLazy;
29
- module.exports.watch = bind('watch');
30
- module.exports.createGenericMonitor = bind('createGenericMonitor');
31
- module.exports.createClaudeMonitor = bind('createClaudeMonitor');
32
- module.exports.createOpenAIMonitor = bind('createOpenAIMonitor');
33
- module.exports.createLangChainHandler = bind('createLangChainHandler');
34
- module.exports.anonymize = bind('anonymize');
35
- module.exports.scrubString = bind('scrubString');
36
- module.exports.hashId = bind('hashId');
package/src/index.js DELETED
@@ -1,26 +0,0 @@
1
- import { WatchMyAgents } from './collector.js';
2
- import { watch, createGenericMonitor } from './adapters/generic.js';
3
- import { createClaudeMonitor } from './adapters/claude.js';
4
- import { createOpenAIMonitor } from './adapters/openai.js';
5
- import { createLangChainHandler } from './adapters/langchain.js';
6
- import { anonymize, scrubString, hashId } from './anonymizer.js';
7
- import { DEFAULT_PRICING, estimateCost, TokenTracker } from './tokens.js';
8
- import * as anthropicManaged from './sources/anthropic-managed.js';
9
-
10
- export {
11
- WatchMyAgents,
12
- watch,
13
- createGenericMonitor,
14
- createClaudeMonitor,
15
- createOpenAIMonitor,
16
- createLangChainHandler,
17
- anonymize,
18
- scrubString,
19
- hashId,
20
- DEFAULT_PRICING,
21
- estimateCost,
22
- TokenTracker,
23
- anthropicManaged,
24
- };
25
-
26
- export default WatchMyAgents;