watchmyagents 1.2.1 → 1.4.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/src/anonymizer.js CHANGED
@@ -156,17 +156,65 @@ export function normalizeToolInput(rawInput, aliases = {}) {
156
156
  // programmatically reference the contract.
157
157
  export { HASHABLE_INPUT_FIELDS };
158
158
 
159
+ // ── Heuristic field-name patterns (v1.3.1 F-30 Codex audit on v1.3.0) ───
160
+ //
161
+ // HASHABLE_INPUT_FIELDS is the canonical set; adapter authors are also
162
+ // encouraged to call `normalizeToolInput()` to map their native names.
163
+ // But OpenAI Agents SDK customers define their own tool argument names
164
+ // (`endpoint_url`, `shell_cmd`, `requestUrl`, …) and rarely think to
165
+ // register aliases. The result before v1.3.1: those fields silently
166
+ // disappeared from Fortress signals — not a leak (the raw payload stays
167
+ // LOCAL), but a detection blind spot.
168
+ //
169
+ // This heuristic catches the long tail. For each non-canonical input
170
+ // field, if its NAME matches a common suffix/word pattern, hash its
171
+ // value as if it were the corresponding canonical field. Conservative
172
+ // by design: regex anchored to word/suffix boundaries (`_url` matches,
173
+ // `urlsafe` does NOT) so we err toward false positives (hashing a
174
+ // harmless field is fine) over false negatives (missing a sensitive
175
+ // field is the bug we're fixing).
176
+ //
177
+ // Add new patterns here when adapters surface new common names.
178
+ const HEURISTIC_FIELD_PATTERNS = Object.freeze({
179
+ url: /(?:^|[_-])(?:url|uri|endpoint|webhook|address)$/i,
180
+ command: /(?:^|[_-])(?:cmd|command|shell|exec|script)$/i,
181
+ path: /(?:^|[_-])(?:path|file|filename|filepath|dir|folder)$/i,
182
+ query: /(?:^|[_-])(?:query|search|q|prompt|term)$/i,
183
+ });
184
+
185
+ function fieldMatchesCanonicalByHeuristic(fieldName, canonical) {
186
+ const pat = HEURISTIC_FIELD_PATTERNS[canonical];
187
+ return pat ? pat.test(fieldName) : false;
188
+ }
189
+
159
190
  // ── Single-entry extractor: what hashable IoCs are in this entry? ────────
160
191
 
161
192
  function extractIocs(entry, salt) {
162
193
  const out = [];
163
194
  if (!entry.input || typeof entry.input !== 'object') return out;
195
+
196
+ // Exact canonical match (original v1.0+ behavior — backwards compat).
164
197
  for (const field of HASHABLE_INPUT_FIELDS) {
165
198
  const v = entry.input[field];
166
199
  if (typeof v === 'string' && v.length > 0) {
167
200
  out.push(hashWithSalt(v, salt));
168
201
  }
169
202
  }
203
+
204
+ // v1.3.1 F-30 — heuristic suffix matching for non-canonical field
205
+ // names common to OpenAI Agents SDK / LangGraph / CrewAI tools. Skip
206
+ // fields already handled above so we never double-hash.
207
+ for (const [key, v] of Object.entries(entry.input)) {
208
+ if (HASHABLE_INPUT_FIELDS.includes(key)) continue;
209
+ if (typeof v !== 'string' || v.length === 0) continue;
210
+ for (const canonical of HASHABLE_INPUT_FIELDS) {
211
+ if (fieldMatchesCanonicalByHeuristic(key, canonical)) {
212
+ out.push(hashWithSalt(v, salt));
213
+ break; // one canonical bucket per field — first match wins
214
+ }
215
+ }
216
+ }
217
+
170
218
  return out;
171
219
  }
172
220
 
package/src/index.js ADDED
@@ -0,0 +1,52 @@
1
+ // WatchMyAgents — main entry point (v1.4 Codex #10).
2
+ //
3
+ // `import { ... } from 'watchmyagents'` resolves here via the package.json
4
+ // `exports['.']` field. For adapter-specific imports (the recommended
5
+ // path for v1.4+), use the sub-entries:
6
+ //
7
+ // import { openaiAgents } from 'watchmyagents/openai-agents';
8
+ //
9
+ // This root entry exposes the most stable cross-adapter primitives:
10
+ // - the source-adapter contract (PROVIDERS, ACTION_TYPES, COMPOSITION_PATTERNS,
11
+ // ENFORCEMENT_MODES, validateWMAAction)
12
+ // - the policy engine + context tracker + decision chain that EVERY
13
+ // adapter shares
14
+ // - the Logger
15
+ //
16
+ // Internal modules (`src/sources/openai-agents-js.js`,
17
+ // `src/shield/*.js`, etc.) remain accessible to advanced consumers via
18
+ // deep imports, but those paths are NOT part of the stable surface;
19
+ // they may move between minor releases. Stick to the sub-entries and
20
+ // this root export for production code.
21
+
22
+ // ── Source-adapter contract ─────────────────────────────────────────
23
+ export {
24
+ PROVIDERS,
25
+ ACTION_TYPES,
26
+ STATUS_VALUES,
27
+ ENFORCEMENT_MODES,
28
+ COMPOSITION_PATTERNS,
29
+ validateWMAAction,
30
+ } from './sources/contract.js';
31
+
32
+ // ── Shield engine (policy eval + context + audit chain) ─────────────
33
+ export {
34
+ evaluate,
35
+ matchesPolicy,
36
+ loadPolicies,
37
+ } from './shield/policy.js';
38
+
39
+ export { createContextTracker, defaultIsError } from './shield/context.js';
40
+
41
+ export {
42
+ createDecisionChain,
43
+ verifyDecisionChain,
44
+ buildGenesisMarker,
45
+ newChainId,
46
+ CHAIN_FIELDS,
47
+ } from './shield/decision-chain.js';
48
+
49
+ export { DecisionLogger } from './shield/decisions.js';
50
+
51
+ // ── Logger ──────────────────────────────────────────────────────────
52
+ export { Logger } from './logger.js';
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: this.agentId,
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
  };
@@ -0,0 +1,202 @@
1
+ // Public entry point for the OpenAI Agents SDK adapter (v1.4 Codex #1).
2
+ //
3
+ // Customers import the SDK like this:
4
+ //
5
+ // import { openaiAgents } from 'watchmyagents/openai-agents';
6
+ //
7
+ // const wma = openaiAgents({
8
+ // policiesPath: './policies.json',
9
+ // logDir: './watchmyagents-logs',
10
+ // mode: 'enforce', // 'observe' | 'enforce' (default 'enforce')
11
+ // toolInputs: {
12
+ // fetch_url: { endpoint_url: 'url' }, // per-tool aliases
13
+ // shell: { shell_cmd: 'command' },
14
+ // },
15
+ // });
16
+ //
17
+ // const agent = new Agent({
18
+ // name: 'support_bot',
19
+ // tools,
20
+ // toolInputGuardrails: [wma.shield()], // ← Shield
21
+ // });
22
+ //
23
+ // wma.watch(agent); // ← Watch (auto-detects Agent vs Runner)
24
+ // // or:
25
+ // wma.watch(runner);
26
+ //
27
+ // await run(agent, 'help me');
28
+ //
29
+ // Why a factory: it lets the customer configure once (policy path,
30
+ // log dir, mode, tool input aliases) and get back a small object with
31
+ // `shield()` and `watch()` methods that share that config. The pre-v1.4
32
+ // pattern asked customers to import deep paths (
33
+ // `watchmyagents/src/sources/openai-agents-js.js`) and pass options to
34
+ // each call — fragile and verbose. This entry point is the stable
35
+ // surface; the internal module stays free to refactor.
36
+
37
+ import {
38
+ wmaToolInputGuardrail,
39
+ attachWmaWatch,
40
+ attachWmaWatchToAgent,
41
+ adapterMeta,
42
+ } from './sources/openai-agents-js.js';
43
+
44
+ /**
45
+ * Build the WMA OpenAI Agents SDK adapter from a single shared config.
46
+ *
47
+ * @param {object} [options]
48
+ * @param {string} [options.policiesPath] Local JSON policy file.
49
+ * @param {object} [options.ruleset] In-memory ruleset.
50
+ * @param {string} [options.logDir] NDJSON log dir.
51
+ * @param {'observe'|'enforce'} [options.mode] v1.4 Codex #2 — explicit
52
+ * mode. `enforce` requires
53
+ * a policy source; `observe`
54
+ * attaches Watch only and
55
+ * refuses to issue a Shield
56
+ * (calling `.shield()` throws
57
+ * so it's impossible to ship
58
+ * a build that looks armed
59
+ * but isn't).
60
+ * @param {object<string, object>} [options.toolInputs]
61
+ * v1.4 Codex #4 — per-tool argument aliases. Maps tool_name →
62
+ * { nativeFieldName: canonicalFieldName }. Applied alongside the
63
+ * F-30 heuristic so customers with off-canonical arg names get
64
+ * exact hashing without relying on the suffix heuristic.
65
+ * @param {boolean} [options.failOpen] Default false (closed).
66
+ * @param {number} [options.maxArgBytes] Default 256 KB.
67
+ * @param {number} [options.maxResultBytes] Default 256 KB.
68
+ * @param {object} [options.tracker] Inject ContextTracker.
69
+ * @param {object} [options.logger] Inject Logger.
70
+ * @param {object} [options.decisionLogger] Inject DecisionLogger.
71
+ * @returns {{
72
+ * shield: () => object,
73
+ * watch: (target: object) => () => void,
74
+ * meta: typeof adapterMeta,
75
+ * }}
76
+ */
77
+ export function openaiAgents(options = {}) {
78
+ const mode = options.mode || 'enforce';
79
+ if (mode !== 'observe' && mode !== 'enforce') {
80
+ throw new TypeError(
81
+ `openaiAgents: options.mode must be 'observe' or 'enforce', got '${mode}'`,
82
+ );
83
+ }
84
+
85
+ // v1.4 Codex #2 — fail-loud refusal to start in enforce mode without
86
+ // any policy source. Avoids the v1.3.0 footgun where missing policies
87
+ // silently degraded to "allow all" with only a stderr warning. The
88
+ // failure surfaces at config time (not on the first request) so it's
89
+ // impossible to deploy a build that LOOKS armed but isn't.
90
+ if (mode === 'enforce' && options.policiesPath == null && options.ruleset == null) {
91
+ throw new Error(
92
+ "openaiAgents({ mode: 'enforce' }) requires policiesPath or ruleset. " +
93
+ "Either provide one, or switch to { mode: 'observe' } if you only " +
94
+ "want Watch (NDJSON capture) without Shield enforcement.",
95
+ );
96
+ }
97
+
98
+ // Shared config we thread into both shield() and watch() so the
99
+ // customer doesn't have to repeat it.
100
+ const sharedOptions = {
101
+ policiesPath: options.policiesPath,
102
+ ruleset: options.ruleset,
103
+ logDir: options.logDir,
104
+ sessionId: options.sessionId,
105
+ failOpen: options.failOpen,
106
+ recentWindowSize: options.recentWindowSize,
107
+ getTeamId: options.getTeamId,
108
+ tracker: options.tracker,
109
+ logger: options.logger,
110
+ decisionLogger: options.decisionLogger,
111
+ toolInputs: options.toolInputs,
112
+ maxArgBytes: options.maxArgBytes,
113
+ maxResultBytes: options.maxResultBytes,
114
+ };
115
+
116
+ return Object.freeze({
117
+ /** Returns the @openai/agents Tool Input Guardrail. In `observe`
118
+ * mode this throws — the customer asked for Watch-only and a
119
+ * guardrail would be misleading. */
120
+ shield() {
121
+ if (mode === 'observe') {
122
+ throw new Error(
123
+ "openaiAgents({ mode: 'observe' }).shield() is not allowed. " +
124
+ "Observe mode does not produce Shield enforcement. " +
125
+ "Switch to { mode: 'enforce', policiesPath: '...' } if you want to block.",
126
+ );
127
+ }
128
+ return wmaToolInputGuardrail(sharedOptions);
129
+ },
130
+
131
+ /** Attaches Watch listeners. Auto-detects whether `target` is an
132
+ * Agent (AgentHooks) or a Runner (RunHooks) by checking method
133
+ * shape and known fields. Returns a detach function. */
134
+ watch(target) {
135
+ return autoAttachWatch(target, sharedOptions);
136
+ },
137
+
138
+ /** Adapter metadata — useful for tooling that wants to introspect
139
+ * what's available (capabilities, peer-dep min version, etc.). */
140
+ meta: adapterMeta,
141
+
142
+ /** The configured mode, exposed for runtime introspection. */
143
+ mode,
144
+ });
145
+ }
146
+
147
+ // v1.4 Codex #7 — auto-detect Runner vs Agent and dispatch to the
148
+ // matching attach function. The shape difference:
149
+ // - @openai/agents Runner has `.run(agent, input)` AND emits the
150
+ // RunHooks events with the agent as an explicit arg.
151
+ // - @openai/agents Agent has `.name`/`.instructions`/`.tools` AND
152
+ // emits AgentHooks events with the agent IMPLICIT (closure-
153
+ // captured by attachWmaWatchToAgent).
154
+ //
155
+ // Both expose `.on()` from the EventEmitter base. We detect the
156
+ // runner by checking for `.run` (function) — it's the convenience the
157
+ // Runner class exposes that Agent doesn't. As a tiebreaker we look
158
+ // at the presence of `.tools` (Agent-only) and the absence of `.name`
159
+ // is fine as a hint but not load-bearing.
160
+ function autoAttachWatch(target, options) {
161
+ if (!target || typeof target.on !== 'function') {
162
+ throw new TypeError(
163
+ 'openaiAgents().watch(target): target must expose .on(event, listener). ' +
164
+ 'Pass an @openai/agents Runner or Agent instance.',
165
+ );
166
+ }
167
+ // Independent signal checks — DO NOT make `looksLikeRunner` exclusive
168
+ // of `looksLikeAgent`. The whole point of the ambiguity check below
169
+ // is to catch targets that satisfy both shapes.
170
+ const hasRunMethod = typeof target.run === 'function';
171
+ const hasAgentShape = Array.isArray(target.tools) && typeof target.name === 'string';
172
+
173
+ if (hasRunMethod && hasAgentShape) {
174
+ throw new TypeError(
175
+ 'openaiAgents().watch(target): target looks like both Agent and Runner ' +
176
+ '(has .tools[] AND .name AND .run() method). Disambiguate by calling ' +
177
+ 'attachWmaWatch(runner) or attachWmaWatchToAgent(agent) directly.',
178
+ );
179
+ }
180
+ if (hasAgentShape) {
181
+ return attachWmaWatchToAgent(target, options);
182
+ }
183
+ if (hasRunMethod) {
184
+ return attachWmaWatch(target, options);
185
+ }
186
+ // Neither shape unambiguously identified — fall back to AgentHooks
187
+ // since the convenience `run(agent, ...)` function dispatches
188
+ // AgentHooks, which is the more common path for new customers
189
+ // (validated against real fixtures captured 2026-06-10).
190
+ return attachWmaWatchToAgent(target, options);
191
+ }
192
+
193
+ // Re-export the low-level functions so customers that need fine-grained
194
+ // control (e.g. test scaffolding, advanced multi-runner setups) can
195
+ // still reach them. The factory is the recommended entry point; these
196
+ // are the escape hatches.
197
+ export {
198
+ wmaToolInputGuardrail,
199
+ attachWmaWatch,
200
+ attachWmaWatchToAgent,
201
+ adapterMeta,
202
+ };
@@ -15,7 +15,17 @@ import { Logger } from '../logger.js';
15
15
  import { createDecisionChain, buildGenesisMarker, newChainId } from './decision-chain.js';
16
16
 
17
17
  export class DecisionLogger {
18
- constructor({ logDir, agentId, sessionId }) {
18
+ // v1.3.1 F-29 (P1 Codex audit on v1.3.0): `provider` is now an
19
+ // explicit constructor option. Before v1.3.1 the provider was
20
+ // hard-coded to `anthropic-managed` in record() — when the OpenAI
21
+ // Agents SDK adapter (shipped v1.3.0) wrote shield_decision rows
22
+ // through this logger, those rows were mis-attributed to Anthropic.
23
+ // Fortress / Guardian forensic surfaces saw OpenAI blocks as
24
+ // Anthropic blocks. Default is preserved at `anthropic-managed` so
25
+ // existing v1.2.x callers behave unchanged; the OpenAI adapter
26
+ // explicitly passes `provider: PROVIDERS.OPENAI_AGENTS`.
27
+ constructor({ logDir, agentId, sessionId, provider }) {
28
+ this._provider = provider || 'anthropic-managed';
19
29
  // Each DecisionLogger instance owns a single chain segment. A Shield
20
30
  // restart creates a fresh DecisionLogger → fresh genesis. The
21
31
  // genesis marker is self-describing (agent + session + start time +
@@ -53,7 +63,7 @@ export class DecisionLogger {
53
63
  && (decision === 'deny' || decision === 'interrupt');
54
64
  return this._logger.write({
55
65
  action_type: 'shield_decision',
56
- provider: 'anthropic-managed',
66
+ provider: this._provider,
57
67
  tool_name: sourceEvent?.name || sourceEvent?.tool_name || null,
58
68
  status: enforced ? 'error' : 'ok',
59
69
  error: enforced ? message : null,
@@ -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'];