watchmyagents 1.2.0 → 1.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/README.md +72 -0
- package/SECURITY.md +1 -1
- package/docs/adapters/openai-agents-js.md +230 -0
- package/examples/adapters/capture-fixtures.ts +193 -0
- package/examples/adapters/openai-agents-js-quickstart.ts +118 -0
- package/examples/policies/mitre-starter.json +133 -0
- package/package.json +16 -1
- package/scripts/shield.js +39 -5
- package/src/logger.js +17 -1
- package/src/sources/contract.js +31 -1
- package/src/sources/openai-agents-js.d.ts +278 -0
- package/src/sources/openai-agents-js.js +733 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment_doc": "WatchMyAgents starter policy bundle — MITRE ATT&CK + ATLAS coverage. Tune to your environment before promoting from mode=shadow → mode=enforce. See docs/MITRE-ATTCK-COVERAGE.md for technique mapping. Every policy ships in shadow mode so the first run only LOGS would-be decisions without blocking — calibrate, then promote.",
|
|
3
|
+
"_comment_lifecycle": "Lifecycle: observe → shadow → enforce → retired. Per [[project_recursive_fractal_loop]], shadow is the calibration bench (Platt scaling + diff-in-diff efficacy). Promote a rule to mode=enforce only after you've seen enough shadow decisions to be confident the rule isn't false-positiving on your workload.",
|
|
4
|
+
"_comment_uses": "Uses v1.2.0+ DSL extensions (gt, in_range, length_gt) and ctx.* namespace (hour_of_day_utc, recent_error_rate, event_count_recent). Falls back gracefully on v1.1.x where unknown operators / ctx paths fail-closed (no false-allow).",
|
|
5
|
+
|
|
6
|
+
"policies": [
|
|
7
|
+
{
|
|
8
|
+
"id": "mitre-t1567-exfil-large-post",
|
|
9
|
+
"name": "T1567 Exfiltration Over Web Service — large outbound POST",
|
|
10
|
+
"action": "deny",
|
|
11
|
+
"mode": "shadow",
|
|
12
|
+
"message": "MITRE T1567: outbound POST exceeds 1MB threshold. Likely exfil channel.",
|
|
13
|
+
"match": {
|
|
14
|
+
"action_type": "tool_use",
|
|
15
|
+
"tool_name": { "in": ["web_fetch", "http_post"] },
|
|
16
|
+
"input.method": "POST",
|
|
17
|
+
"input.bytes": { "gt": 1000000 }
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
"id": "mitre-t1041-c2-host-allowlist",
|
|
23
|
+
"name": "T1041 Exfiltration Over C2 Channel — host allowlist",
|
|
24
|
+
"action": "deny",
|
|
25
|
+
"mode": "shadow",
|
|
26
|
+
"message": "MITRE T1041: outbound to non-allowlisted host. Replace the regex with your trusted egress hosts.",
|
|
27
|
+
"match": {
|
|
28
|
+
"action_type": "tool_use",
|
|
29
|
+
"tool_name": { "in": ["web_fetch", "http_post"] },
|
|
30
|
+
"input.url": { "not_regex": "^https://(api\\.openai\\.com|api\\.anthropic\\.com|github\\.com|api\\.github\\.com|raw\\.githubusercontent\\.com|wikipedia\\.org|en\\.wikipedia\\.org)(/|$|:[0-9]+)" }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"id": "mitre-t1485-data-destruction",
|
|
36
|
+
"name": "T1485 Data Destruction — destructive shell patterns",
|
|
37
|
+
"action": "deny",
|
|
38
|
+
"mode": "shadow",
|
|
39
|
+
"message": "MITRE T1485: destructive shell pattern (rm -rf / mkfs / dd if=).",
|
|
40
|
+
"match": {
|
|
41
|
+
"tool_name": "bash",
|
|
42
|
+
"input.command": { "regex": "(^|\\s|;|&&|\\|\\|)(rm\\s+-rf\\s|mkfs[. ]|dd\\s+if=)" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
"id": "mitre-t1548-privilege-escalation",
|
|
48
|
+
"name": "T1548 Abuse Elevation Control — sudo / pkexec / doas",
|
|
49
|
+
"action": "interrupt",
|
|
50
|
+
"mode": "shadow",
|
|
51
|
+
"message": "MITRE T1548: privilege-escalation attempt via sudo / pkexec / doas.",
|
|
52
|
+
"match": {
|
|
53
|
+
"tool_name": "bash",
|
|
54
|
+
"input.command": { "regex": "(^|\\s|;|&&|\\|\\|)(sudo|pkexec|doas)\\s" }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
"id": "mitre-t1059-bash-command-length",
|
|
60
|
+
"name": "T1059 Command and Scripting Interpreter — improbably long command",
|
|
61
|
+
"action": "deny",
|
|
62
|
+
"mode": "shadow",
|
|
63
|
+
"message": "MITRE T1059: bash command exceeds 2KB — likely a base64-encoded payload or pipeline obfuscation.",
|
|
64
|
+
"match": {
|
|
65
|
+
"tool_name": "bash",
|
|
66
|
+
"input.command": { "length_gt": 2048 }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
"id": "mitre-t1083-after-hours-discovery",
|
|
72
|
+
"name": "T1083 File and Directory Discovery — outside business hours (late)",
|
|
73
|
+
"action": "interrupt",
|
|
74
|
+
"mode": "shadow",
|
|
75
|
+
"message": "MITRE T1083: file discovery between 22:00 and 23:59 UTC. Tune for your business hours.",
|
|
76
|
+
"match": {
|
|
77
|
+
"tool_name": { "in": ["bash", "file_list"] },
|
|
78
|
+
"ctx.hour_of_day_utc": { "in_range": [22, 23] }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
"id": "mitre-t1083-after-hours-discovery-early",
|
|
84
|
+
"name": "T1083 File and Directory Discovery — outside business hours (early)",
|
|
85
|
+
"action": "interrupt",
|
|
86
|
+
"mode": "shadow",
|
|
87
|
+
"message": "MITRE T1083: file discovery between 00:00 and 06:59 UTC. Tune for your business hours.",
|
|
88
|
+
"match": {
|
|
89
|
+
"tool_name": { "in": ["bash", "file_list"] },
|
|
90
|
+
"ctx.hour_of_day_utc": { "in_range": [0, 6] }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"id": "mitre-t1020-flailing-agent-throttle",
|
|
96
|
+
"name": "T1020 Automated Exfiltration — throttle flailing agent (high error rate)",
|
|
97
|
+
"action": "interrupt",
|
|
98
|
+
"mode": "shadow",
|
|
99
|
+
"message": "MITRE T1020: agent recent_error_rate > 0.5 across last >= 4 events — likely a runaway loop or retry storm.",
|
|
100
|
+
"match": {
|
|
101
|
+
"action_type": "tool_use",
|
|
102
|
+
"ctx.recent_error_rate": { "gt": 0.5 },
|
|
103
|
+
"ctx.event_count_recent": { "gte": 4 }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
"id": "mitre-t1053-cron-persistence",
|
|
109
|
+
"name": "T1053 Scheduled Task/Job — cron / launchd persistence",
|
|
110
|
+
"action": "deny",
|
|
111
|
+
"mode": "shadow",
|
|
112
|
+
"message": "MITRE T1053: agent attempted to write a persistent task. Possible C2 persistence.",
|
|
113
|
+
"match": {
|
|
114
|
+
"tool_name": { "in": ["bash", "file_write"] },
|
|
115
|
+
"input.path": { "regex": "(crontab|/etc/cron\\.[a-z]+/|\\.crontab|/Library/LaunchAgents/|/Library/LaunchDaemons/|\\.service$|/etc/systemd/system/)" }
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
"id": "mitre-t1552-credential-store-read",
|
|
121
|
+
"name": "T1552 Unsecured Credentials — read of well-known credential store",
|
|
122
|
+
"action": "deny",
|
|
123
|
+
"mode": "shadow",
|
|
124
|
+
"message": "MITRE T1552: agent attempted to read a credential store. Tighten the path regex for your environment.",
|
|
125
|
+
"match": {
|
|
126
|
+
"tool_name": { "in": ["bash", "file_read"] },
|
|
127
|
+
"input.path": { "regex": "(/\\.aws/credentials|/\\.aws/config|/\\.ssh/id_[a-z]+|/\\.netrc|Library/Keychains|/etc/shadow)" }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
|
|
132
|
+
"default": { "action": "allow" }
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "watchmyagents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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": [
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"scripts/upload-fortress.js",
|
|
13
13
|
"scripts/service.js",
|
|
14
14
|
"scripts/agents.js",
|
|
15
|
+
"examples/policies/",
|
|
16
|
+
"examples/adapters/",
|
|
17
|
+
"docs/adapters/",
|
|
15
18
|
"README.md",
|
|
16
19
|
"SECURITY.md",
|
|
17
20
|
"LICENSE"
|
|
@@ -40,6 +43,14 @@
|
|
|
40
43
|
},
|
|
41
44
|
"dependencies": {},
|
|
42
45
|
"devDependencies": {},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@openai/agents": "^0.2.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@openai/agents": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
43
54
|
"keywords": [
|
|
44
55
|
"ai",
|
|
45
56
|
"agents",
|
|
@@ -50,7 +61,11 @@
|
|
|
50
61
|
"cybersecurity",
|
|
51
62
|
"anthropic",
|
|
52
63
|
"claude",
|
|
64
|
+
"openai",
|
|
65
|
+
"openai-agents",
|
|
53
66
|
"managed-agents",
|
|
67
|
+
"agent-runtime",
|
|
68
|
+
"guardrails",
|
|
54
69
|
"audit",
|
|
55
70
|
"ndjson",
|
|
56
71
|
"policy-enforcement",
|
package/scripts/shield.js
CHANGED
|
@@ -203,18 +203,35 @@ async function runSessionWorker({ sessionId, ctx }) {
|
|
|
203
203
|
})) {
|
|
204
204
|
processed++;
|
|
205
205
|
|
|
206
|
+
// v1.2.1 F-26 (P1 Codex audit on v1.2.0): record EVERY event
|
|
207
|
+
// flowing through Shield, not just the cacheable tool_use ones.
|
|
208
|
+
// The previous wiring only called record() inside the cacheable
|
|
209
|
+
// branches, so events that DO carry the is_error / error / content
|
|
210
|
+
// tool_result markers (which arrive as a DIFFERENT event type
|
|
211
|
+
// from the upstream tool_use) never reached the tracker. Result
|
|
212
|
+
// in production: ctx.recent_error_rate stayed pinned at 0 even
|
|
213
|
+
// when the agent's tools were failing constantly, making the
|
|
214
|
+
// "throttle a flailing agent" policy class effectively a no-op.
|
|
215
|
+
//
|
|
216
|
+
// Implementation note: try/finally guarantees record() runs even
|
|
217
|
+
// when the cacheable branches `continue;` out mid-iteration. The
|
|
218
|
+
// finally executes BEFORE the continue takes effect (JS spec), so
|
|
219
|
+
// the compute() calls below — which must see PRIOR history only —
|
|
220
|
+
// still observe the correct window.
|
|
221
|
+
try {
|
|
222
|
+
|
|
206
223
|
// ── INTERRUPT MODE ──────────────────────────────────────────────
|
|
207
224
|
if (mode === 'interrupt' && CACHEABLE_TOOL_TYPES.has(rawEvent.type)) {
|
|
208
225
|
// No caching in interrupt mode — react synchronously, free memory.
|
|
209
226
|
const normalized = normalizeForPolicy(rawEvent);
|
|
210
227
|
// v1.2.0 — compute policy context BEFORE evaluate so rules see
|
|
211
|
-
// PRIOR history (recent_error_rate, agent_age_minutes, …)
|
|
212
|
-
//
|
|
228
|
+
// PRIOR history (recent_error_rate, agent_age_minutes, …). The
|
|
229
|
+
// finally at the end of this iteration records the event so the
|
|
230
|
+
// NEXT iteration sees this one in its window.
|
|
213
231
|
const policyCtx = policyTracker.compute(rawEvent);
|
|
214
232
|
const t0 = Date.now();
|
|
215
233
|
const result = evaluate(normalized, ctx.ruleset, policyCtx);
|
|
216
234
|
const decidedInMs = Date.now() - t0;
|
|
217
|
-
policyTracker.record(rawEvent);
|
|
218
235
|
|
|
219
236
|
// v1.1.3 Phase 1.D — mode badge in the log line for operator visibility.
|
|
220
237
|
const modeTag = result.mode === 'shadow' ? ' [SHADOW]' : '';
|
|
@@ -282,12 +299,14 @@ async function runSessionWorker({ sessionId, ctx }) {
|
|
|
282
299
|
|
|
283
300
|
const normalized = normalizeForPolicy(sourceEvent);
|
|
284
301
|
// v1.2.0 — see interrupt-mode block above for the compute /
|
|
285
|
-
// record ordering rationale.
|
|
302
|
+
// record ordering rationale. sourceEvent here was the original
|
|
303
|
+
// tool_use rawEvent at an earlier loop iteration, and was
|
|
304
|
+
// recorded by THAT iteration's finally — so compute() correctly
|
|
305
|
+
// sees it as PRIOR history, not as the current event.
|
|
286
306
|
const policyCtx = policyTracker.compute(sourceEvent);
|
|
287
307
|
const t0 = Date.now();
|
|
288
308
|
const result = evaluate(normalized, ctx.ruleset, policyCtx);
|
|
289
309
|
const decidedInMs = Date.now() - t0;
|
|
290
|
-
policyTracker.record(sourceEvent);
|
|
291
310
|
|
|
292
311
|
// v1.1.3 Phase 1.D — mode badge in the log line for operator visibility.
|
|
293
312
|
const modeTag = result.mode === 'shadow' ? ' [SHADOW]' : '';
|
|
@@ -350,6 +369,21 @@ async function runSessionWorker({ sessionId, ctx }) {
|
|
|
350
369
|
sinfo(sessionId, `session terminated: ${rawEvent.stop_reason?.type || 'unknown'}`);
|
|
351
370
|
break;
|
|
352
371
|
}
|
|
372
|
+
|
|
373
|
+
} finally {
|
|
374
|
+
// v1.2.1 F-26: record EVERY event into the policy tracker so
|
|
375
|
+
// recent_error_rate reflects real tool failures (which arrive
|
|
376
|
+
// as tool_result events carrying is_error: true, NOT as the
|
|
377
|
+
// upstream tool_use events). defaultIsError() inside the
|
|
378
|
+
// tracker handles the is_error / error / content[].is_error /
|
|
379
|
+
// type-contains-'error' shapes — see src/shield/context.js.
|
|
380
|
+
//
|
|
381
|
+
// Order: every compute() call above happens BEFORE this
|
|
382
|
+
// record() (JS guarantees finally runs after try-body but
|
|
383
|
+
// before continue/break take effect), so the "PRIOR history"
|
|
384
|
+
// contract of recent_error_rate is preserved.
|
|
385
|
+
policyTracker.record(rawEvent);
|
|
386
|
+
}
|
|
353
387
|
}
|
|
354
388
|
} catch (e) {
|
|
355
389
|
if (!signal.aborted) {
|
package/src/logger.js
CHANGED
|
@@ -33,6 +33,10 @@ const EXPORT_FIELDS = [
|
|
|
33
33
|
'cost_usd', 'model',
|
|
34
34
|
'session_tokens', 'session_cost_usd',
|
|
35
35
|
'status', 'error', 'sequence_number', 'session_id',
|
|
36
|
+
// v1.3.0 — team correlation across cooperating agents. Populated by
|
|
37
|
+
// adapters that observe handoffs (OpenAI Agents SDK, future Claude
|
|
38
|
+
// Code workflow runId). Null when unknown / no team scope.
|
|
39
|
+
'team_id',
|
|
36
40
|
];
|
|
37
41
|
|
|
38
42
|
export class Logger {
|
|
@@ -78,7 +82,15 @@ export class Logger {
|
|
|
78
82
|
const path = this._pathForToday();
|
|
79
83
|
const full = {
|
|
80
84
|
id: e.id || randomUUID(),
|
|
81
|
-
agent_id
|
|
85
|
+
// v1.3.0 — prefer the event's agent_id when present so multi-agent
|
|
86
|
+
// adapters (OpenAI Agents SDK handoffs, future LangGraph subgraphs)
|
|
87
|
+
// can attribute each event to its actual emitter. Falls back to
|
|
88
|
+
// the Logger's constructor agentId for single-agent adapters
|
|
89
|
+
// (Anthropic Managed) where every event from this logger belongs
|
|
90
|
+
// to the same agent. Backwards compatible: existing Anthropic
|
|
91
|
+
// adapter doesn't populate e.agent_id on every yield, so the
|
|
92
|
+
// fallback path keeps its behavior intact.
|
|
93
|
+
agent_id: e.agent_id || this.agentId,
|
|
82
94
|
// PR-C: sub-agent fields. Defaults are honest for solo / root agents.
|
|
83
95
|
// An adapter that detects hierarchy (e.g. OpenAI Agents handoffs)
|
|
84
96
|
// populates these on the event, and the Logger threads them through.
|
|
@@ -107,6 +119,10 @@ export class Logger {
|
|
|
107
119
|
session_id: this.sessionId,
|
|
108
120
|
session_tokens: e.session_tokens ?? null,
|
|
109
121
|
session_cost_usd: e.session_cost_usd ?? null,
|
|
122
|
+
// v1.3.0 — team correlation. Adapters that observe multi-agent
|
|
123
|
+
// groupings (OpenAI Agents SDK handoffs, future Claude Code
|
|
124
|
+
// workflow runId) populate this. Null when unknown.
|
|
125
|
+
team_id: e.team_id ?? null,
|
|
110
126
|
input: e.input ?? null,
|
|
111
127
|
output: e.output ?? null,
|
|
112
128
|
};
|
package/src/sources/contract.js
CHANGED
|
@@ -140,9 +140,15 @@ export const COMPOSITION_PATTERNS = Object.freeze({
|
|
|
140
140
|
// name here as they land so consumers can build provider-specific UI.
|
|
141
141
|
export const PROVIDERS = Object.freeze({
|
|
142
142
|
ANTHROPIC_MANAGED: 'anthropic-managed',
|
|
143
|
+
// v1.3.0 (Phase 2.A) — OpenAI Agents SDK (TypeScript/JS).
|
|
144
|
+
// Customer-instrumented (not auto-discovery). Push-model via:
|
|
145
|
+
// - Tool Input Guardrails (Shield enforcement: allow/deny/interrupt)
|
|
146
|
+
// - RunHooks EventEmitter (Watch observability)
|
|
147
|
+
// See `./openai-agents-js.js` and `docs/adapters/openai-agents-js.md`.
|
|
148
|
+
OPENAI_AGENTS: 'openai-agents',
|
|
143
149
|
// Coming next:
|
|
144
|
-
// OPENAI_AGENTS: 'openai-agents',
|
|
145
150
|
// AWS_BEDROCK_AGENTCORE: 'aws-bedrock-agentcore',
|
|
151
|
+
// CLAUDE_CODE: 'claude-code',
|
|
146
152
|
// LANGGRAPH: 'langgraph',
|
|
147
153
|
// CREWAI: 'crewai',
|
|
148
154
|
});
|
|
@@ -190,6 +196,30 @@ export const PROVIDERS = Object.freeze({
|
|
|
190
196
|
// * @property {string|null} agent_name The human-named emitter of this event
|
|
191
197
|
// * (the parent agent OR a sub-agent
|
|
192
198
|
// * running inside the parent's session).
|
|
199
|
+
// *
|
|
200
|
+
// * TEAM CORRELATION (v1.3.0 — Phase 2.A OpenAI Agents SDK + future
|
|
201
|
+
// * Claude Code dynamic workflows):
|
|
202
|
+
// * @property {string|null} team_id Stable identifier shared by every
|
|
203
|
+
// * event in a logical group of
|
|
204
|
+
// * cooperating agents. Sources:
|
|
205
|
+
// * (a) customer override via
|
|
206
|
+
// * WMA_TEAM_ID env var or
|
|
207
|
+
// * programmatic setter,
|
|
208
|
+
// * (b) auto-detected from the
|
|
209
|
+
// * OpenAI Agents SDK
|
|
210
|
+
// * `agent_handoff` event —
|
|
211
|
+
// * all agents in the handoff
|
|
212
|
+
// * chain share the same
|
|
213
|
+
// * team_id,
|
|
214
|
+
// * (c) future: Claude Code dynamic
|
|
215
|
+
// * workflow `runId`.
|
|
216
|
+
// * Drives the Legions UI "Team view"
|
|
217
|
+
// * drill-down. Null when unknown.
|
|
218
|
+
// *
|
|
219
|
+
// * RUNTIME-COMPUTED CONTEXT (v1.2.0 — see src/shield/context.js):
|
|
220
|
+
// * ctx attributes (hour_of_day_utc, recent_error_rate, etc.) are NOT stored
|
|
221
|
+
// * on WMAAction — they're computed at policy eval time and live in the
|
|
222
|
+
// * ContextTracker only.
|
|
193
223
|
// */
|
|
194
224
|
|
|
195
225
|
const REQUIRED_FIELDS = ['id', 'provider', 'agent_id', 'session_id', 'action_type', 'timestamp', 'status'];
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// TypeScript types for the OpenAI Agents SDK adapter — v1.3.0.
|
|
2
|
+
//
|
|
3
|
+
// These types pin the PUBLIC surface customers consume. The runtime
|
|
4
|
+
// (openai-agents-js.js) is plain JS to preserve the zero-runtime-deps
|
|
5
|
+
// invariant; this .d.ts is consumed by tsc only and never executed.
|
|
6
|
+
//
|
|
7
|
+
// The guardrail return shape and the EventEmitter event signatures
|
|
8
|
+
// mirror @openai/agents v0.x — we don't import from it (also to avoid
|
|
9
|
+
// dragging it as a runtime dep here), we re-declare the minimal subset
|
|
10
|
+
// we need.
|
|
11
|
+
|
|
12
|
+
// ── @openai/agents shapes we reference (minimal re-declaration) ────────
|
|
13
|
+
|
|
14
|
+
/** Mirrors @openai/agents `ToolGuardrailBehavior`. */
|
|
15
|
+
export type WMAToolGuardrailBehavior =
|
|
16
|
+
| { type: 'allow' }
|
|
17
|
+
| { type: 'rejectContent'; message: string }
|
|
18
|
+
| { type: 'throwException' };
|
|
19
|
+
|
|
20
|
+
/** Mirrors @openai/agents `ToolGuardrailFunctionOutput`. */
|
|
21
|
+
export interface WMAToolGuardrailFunctionOutput {
|
|
22
|
+
behavior: WMAToolGuardrailBehavior;
|
|
23
|
+
outputInfo?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Mirrors @openai/agents `ToolInputGuardrailData`. */
|
|
27
|
+
export interface WMAToolInputGuardrailData {
|
|
28
|
+
context?: unknown;
|
|
29
|
+
agent?: { name?: string; model?: string; instructions?: string };
|
|
30
|
+
toolCall?: {
|
|
31
|
+
id?: string;
|
|
32
|
+
callId?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
arguments?: string | object;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return value of {@link wmaToolInputGuardrail}. Shape-compatible with
|
|
40
|
+
* @openai/agents `ToolInputGuardrailDefinition`. Drop into an Agent's
|
|
41
|
+
* `toolInputGuardrails: [...]` array.
|
|
42
|
+
*/
|
|
43
|
+
export interface WMAToolInputGuardrail {
|
|
44
|
+
readonly type: 'tool_input';
|
|
45
|
+
readonly name: string;
|
|
46
|
+
run: (data: WMAToolInputGuardrailData) => Promise<WMAToolGuardrailFunctionOutput>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Adapter-specific types ─────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** Minimal Logger surface — for advanced injection in tests. */
|
|
52
|
+
export interface WMALogger {
|
|
53
|
+
write(event: unknown): Promise<unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Minimal DecisionLogger surface. */
|
|
57
|
+
export interface WMADecisionLogger {
|
|
58
|
+
record(args: {
|
|
59
|
+
sourceEvent: unknown;
|
|
60
|
+
decision: string;
|
|
61
|
+
ruleId: string | null;
|
|
62
|
+
ruleName: string | null;
|
|
63
|
+
message: string | null;
|
|
64
|
+
decidedInMs: number | null;
|
|
65
|
+
mode: 'enforce' | 'shadow';
|
|
66
|
+
}): Promise<unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Minimal ContextTracker surface. */
|
|
70
|
+
export interface WMAContextTracker {
|
|
71
|
+
compute(event: unknown, at?: number): {
|
|
72
|
+
hour_of_day_utc: number;
|
|
73
|
+
day_of_week_utc: number;
|
|
74
|
+
agent_age_minutes: number;
|
|
75
|
+
session_duration_ms: number;
|
|
76
|
+
recent_error_rate: number;
|
|
77
|
+
event_count_recent: number;
|
|
78
|
+
event_count_total: number;
|
|
79
|
+
};
|
|
80
|
+
record(event: unknown, opts?: { isError?: boolean }): void;
|
|
81
|
+
reset(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Minimal team tracker for handoff propagation. */
|
|
85
|
+
export interface WMATeamTracker {
|
|
86
|
+
resolve(sessionId: string): string | null;
|
|
87
|
+
bootstrap(sessionId: string): string;
|
|
88
|
+
recordHandoff(fromAgent: unknown, toAgent: unknown, sessionId: string): string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Options for {@link wmaToolInputGuardrail}. */
|
|
92
|
+
export interface WMAGuardrailOptions {
|
|
93
|
+
/** Path to a local JSON policy file. Loaded lazily on first invocation. */
|
|
94
|
+
policiesPath?: string;
|
|
95
|
+
/** In-memory ruleset (overrides policiesPath). Useful for tests. */
|
|
96
|
+
ruleset?: { policies: unknown[]; default?: { action: string } };
|
|
97
|
+
/** Directory for NDJSON logs. Defaults to env WMA_LOG_DIR or ./watchmyagents-logs. */
|
|
98
|
+
logDir?: string;
|
|
99
|
+
/** Override the session id. Defaults to an auto-minted UUID. */
|
|
100
|
+
sessionId?: string;
|
|
101
|
+
/** Trailing window for `ctx.recent_error_rate` etc. Default: 20. */
|
|
102
|
+
recentWindowSize?: number;
|
|
103
|
+
/** On Shield internal error, allow vs deny. Default: false (fail-CLOSED). */
|
|
104
|
+
failOpen?: boolean;
|
|
105
|
+
/** Custom team id resolver — overrides env + handoff propagation. */
|
|
106
|
+
getTeamId?: () => string | null;
|
|
107
|
+
/** Inject an existing Logger (testing). */
|
|
108
|
+
logger?: WMALogger;
|
|
109
|
+
/** Inject an existing DecisionLogger (testing). */
|
|
110
|
+
decisionLogger?: WMADecisionLogger;
|
|
111
|
+
/** Inject an existing ContextTracker (testing). */
|
|
112
|
+
tracker?: WMAContextTracker;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the WMA Shield as an @openai/agents Tool Input Guardrail.
|
|
117
|
+
*
|
|
118
|
+
* Usage:
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import { Agent } from '@openai/agents';
|
|
121
|
+
* import { wmaToolInputGuardrail } from 'watchmyagents';
|
|
122
|
+
*
|
|
123
|
+
* const wmaShield = wmaToolInputGuardrail({
|
|
124
|
+
* policiesPath: './policies/mitre-starter.json',
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* const agent = new Agent({
|
|
128
|
+
* name: 'Support bot',
|
|
129
|
+
* instructions: '...',
|
|
130
|
+
* tools: [...],
|
|
131
|
+
* toolInputGuardrails: [wmaShield],
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export function wmaToolInputGuardrail(options?: WMAGuardrailOptions): WMAToolInputGuardrail;
|
|
136
|
+
|
|
137
|
+
// ── Watch ──────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Minimal Runner surface (the part of @openai/agents we touch). */
|
|
140
|
+
export interface WMARunnerLike {
|
|
141
|
+
on(event: string, listener: (...args: any[]) => void): unknown;
|
|
142
|
+
off?(event: string, listener: (...args: any[]) => void): unknown;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Minimal Agent surface — same shape as Runner from an EventEmitter
|
|
146
|
+
* perspective. The methods are identical; the difference is the listener
|
|
147
|
+
* signatures (see attachWmaWatchToAgent). */
|
|
148
|
+
export interface WMAAgentLike {
|
|
149
|
+
on(event: string, listener: (...args: any[]) => void): unknown;
|
|
150
|
+
off?(event: string, listener: (...args: any[]) => void): unknown;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Options for {@link attachWmaWatch}. */
|
|
154
|
+
export interface WMAWatchOptions {
|
|
155
|
+
logDir?: string;
|
|
156
|
+
sessionId?: string;
|
|
157
|
+
logger?: WMALogger;
|
|
158
|
+
teamTracker?: WMATeamTracker;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Attach WMA Watch listeners to an @openai/agents Runner. Returns a
|
|
163
|
+
* detach function the customer can call on shutdown.
|
|
164
|
+
*
|
|
165
|
+
* USE THIS when the customer creates a Runner explicitly:
|
|
166
|
+
* ```typescript
|
|
167
|
+
* import { Runner } from '@openai/agents';
|
|
168
|
+
* import { attachWmaWatch } from 'watchmyagents';
|
|
169
|
+
*
|
|
170
|
+
* const runner = new Runner();
|
|
171
|
+
* const detach = attachWmaWatch(runner);
|
|
172
|
+
*
|
|
173
|
+
* await runner.run(agent, input);
|
|
174
|
+
*
|
|
175
|
+
* detach(); // optional, on process shutdown
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* If the customer uses the convenience `run(agent, ...)` function
|
|
179
|
+
* (no explicit Runner), use {@link attachWmaWatchToAgent} instead.
|
|
180
|
+
*/
|
|
181
|
+
export function attachWmaWatch(
|
|
182
|
+
runner: WMARunnerLike,
|
|
183
|
+
options?: WMAWatchOptions,
|
|
184
|
+
): () => void;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Attach WMA Watch listeners to an @openai/agents Agent (AgentHooks
|
|
188
|
+
* pattern). Use this when the customer uses the convenience
|
|
189
|
+
* `run(agent, ...)` function rather than creating an explicit Runner.
|
|
190
|
+
*
|
|
191
|
+
* The AgentHooks event signatures differ slightly from RunHooks:
|
|
192
|
+
* agent_end, agent_handoff, agent_tool_start, agent_tool_end do NOT
|
|
193
|
+
* pass the agent in their args — it's captured via closure here.
|
|
194
|
+
*
|
|
195
|
+
* Usage:
|
|
196
|
+
* ```typescript
|
|
197
|
+
* import { Agent, run } from '@openai/agents';
|
|
198
|
+
* import { attachWmaWatchToAgent } from 'watchmyagents';
|
|
199
|
+
*
|
|
200
|
+
* const myAgent = new Agent({ name: 'support_bot', tools: [...] });
|
|
201
|
+
* const detach = attachWmaWatchToAgent(myAgent);
|
|
202
|
+
*
|
|
203
|
+
* await run(myAgent, 'help me reset my password');
|
|
204
|
+
*
|
|
205
|
+
* detach(); // optional, on process shutdown
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
export function attachWmaWatchToAgent(
|
|
209
|
+
agent: WMAAgentLike,
|
|
210
|
+
options?: WMAWatchOptions,
|
|
211
|
+
): () => void;
|
|
212
|
+
|
|
213
|
+
// ── Normalizers (exported for advanced users / tests) ──────────────────
|
|
214
|
+
|
|
215
|
+
/** Mirrors the WMA contract `WMAAction` shape. */
|
|
216
|
+
export interface WMAAction {
|
|
217
|
+
id: string;
|
|
218
|
+
provider: string;
|
|
219
|
+
agent_id: string;
|
|
220
|
+
agent_name: string | null;
|
|
221
|
+
session_id: string;
|
|
222
|
+
session_thread_id: string | null;
|
|
223
|
+
action_type: string;
|
|
224
|
+
timestamp: string;
|
|
225
|
+
status: 'ok' | 'error' | 'blocked';
|
|
226
|
+
tool_name: string | null;
|
|
227
|
+
model: string | null;
|
|
228
|
+
duration_ms: number | null;
|
|
229
|
+
parent_agent_id: string | null;
|
|
230
|
+
composition_pattern: 'solo' | 'hierarchy' | 'graph' | 'peer';
|
|
231
|
+
team_id: string | null;
|
|
232
|
+
input: object | null;
|
|
233
|
+
output: object | null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface NormalizeArgs<T extends string> {
|
|
237
|
+
sessionId: string;
|
|
238
|
+
teamId: string | null;
|
|
239
|
+
agent?: { name?: string; model?: string };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function normalizeAgentStart(args: NormalizeArgs<'agent_start'> & { turnInput?: unknown[] }): Readonly<WMAAction>;
|
|
243
|
+
export function normalizeAgentEnd(args: NormalizeArgs<'agent_end'> & { output?: string }): Readonly<WMAAction>;
|
|
244
|
+
export function normalizeAgentHandoff(args: NormalizeArgs<'agent_handoff'> & {
|
|
245
|
+
fromAgent?: { name?: string; model?: string };
|
|
246
|
+
toAgent?: { name?: string; model?: string };
|
|
247
|
+
}): Readonly<WMAAction>;
|
|
248
|
+
export function normalizeToolStart(args: NormalizeArgs<'tool_start'> & {
|
|
249
|
+
tool?: { name?: string };
|
|
250
|
+
toolCall?: { id?: string; callId?: string; name?: string; arguments?: string | object };
|
|
251
|
+
}): Readonly<WMAAction>;
|
|
252
|
+
export function normalizeToolEnd(args: NormalizeArgs<'tool_end'> & {
|
|
253
|
+
tool?: { name?: string };
|
|
254
|
+
toolCall?: { id?: string; callId?: string; name?: string; arguments?: string | object };
|
|
255
|
+
result?: unknown;
|
|
256
|
+
}): Readonly<WMAAction>;
|
|
257
|
+
|
|
258
|
+
// ── Adapter metadata ───────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
export interface WMAAdapterMeta {
|
|
261
|
+
readonly provider: 'openai-agents';
|
|
262
|
+
readonly displayName: string;
|
|
263
|
+
readonly enforcement: 'sync_confirm' | 'sync_interrupt' | 'detect_only';
|
|
264
|
+
readonly compositionDefault: 'solo' | 'hierarchy' | 'graph' | 'peer';
|
|
265
|
+
readonly customerInstrumented: true;
|
|
266
|
+
readonly peerPackage: '@openai/agents';
|
|
267
|
+
readonly minPeerVersion: string;
|
|
268
|
+
readonly capabilities: Readonly<{
|
|
269
|
+
watch: boolean;
|
|
270
|
+
shield: boolean;
|
|
271
|
+
preToolDeny: boolean;
|
|
272
|
+
postToolFilter: boolean;
|
|
273
|
+
teamIdAutoDetect: boolean;
|
|
274
|
+
streamingSupported: boolean | string;
|
|
275
|
+
}>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const adapterMeta: WMAAdapterMeta;
|