watchmyagents 1.3.0 → 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.
@@ -196,7 +196,7 @@ Tool arguments and results stay LOCAL. The anonymizer converts WMAAction → sig
196
196
 
197
197
  - Node.js: **20+** (matches the WMA SDK baseline and `@openai/agents`'s requirement)
198
198
  - TypeScript: any version — types live in `src/sources/openai-agents-js.d.ts`
199
- - `@openai/agents` version range: tested against `^x.y` (verified in CI against the version pinned in our integration fixtures — see `test/fixtures/openai-agents-events/README.md`)
199
+ - `@openai/agents` version range: **`^0.2.0`** (declared in `package.json#peerDependencies`). Real fixtures in `test/fixtures/openai-agents-events/` were captured against 0.2.x and the adapter's lifecycle event signatures match that line. Older 0.1.x lacked AgentHooks ergonomics the adapter relies on.
200
200
 
201
201
  ## Limits + known gaps (v1.3.0)
202
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "1.3.0",
3
+ "version": "1.4.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": [
@@ -19,6 +19,14 @@
19
19
  "SECURITY.md",
20
20
  "LICENSE"
21
21
  ],
22
+ "exports": {
23
+ ".": "./src/index.js",
24
+ "./openai-agents": {
25
+ "types": "./src/sources/openai-agents-js.d.ts",
26
+ "default": "./src/openai-agents.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
22
30
  "bin": {
23
31
  "wma-inspect": "scripts/inspect.js",
24
32
  "wma-fetch": "scripts/fetch-anthropic.js",
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';
@@ -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,
@@ -55,6 +55,7 @@ import { evaluate, loadPolicies } from '../shield/policy.js';
55
55
  import { createContextTracker } from '../shield/context.js';
56
56
  import { DecisionLogger } from '../shield/decisions.js';
57
57
  import { Logger } from '../logger.js';
58
+ import { normalizeToolInput } from '../anonymizer.js';
58
59
 
59
60
  // ── Constants ──────────────────────────────────────────────────────────
60
61
 
@@ -65,6 +66,18 @@ const PROVIDER = PROVIDERS.OPENAI_AGENTS;
65
66
  // the customer must opt into explicitly, with all the implications.
66
67
  const DEFAULT_FAIL_OPEN = false;
67
68
 
69
+ // v1.4 F-32 (P2 Codex audit on v1.3.0): bound the bytes we accept on
70
+ // the hot path. A misbehaving tool, a model hallucinating a huge
71
+ // response, or a malicious customer-controlled fixture can otherwise
72
+ // pin CPU on JSON.parse, blow up the NDJSON line, or fill the disk.
73
+ // 256 KB is generous for legitimate tool inputs/outputs (real-world
74
+ // captures from the Guardian agent peak at < 2 KB) and well under what
75
+ // would degrade Watch / Shield. Customers with outlier traffic can
76
+ // override per-guardrail via options.maxArgBytes / options.maxResultBytes.
77
+ const DEFAULT_MAX_ARG_BYTES = 256 * 1024;
78
+ const DEFAULT_MAX_RESULT_BYTES = 256 * 1024;
79
+ const TRUNCATION_SENTINEL = '…[truncated by WMA Shield]';
80
+
68
81
  // Mirror of '@openai/agents' value object factory. Replicated here so we
69
82
  // don't take a runtime dep. If '@openai/agents' ever changes the shape
70
83
  // (extreme low probability), fixture tests will catch it.
@@ -95,10 +108,44 @@ function readEnv(key, fallback) {
95
108
  // as a JSON string; some custom tool implementations pass them through
96
109
  // already-parsed. Fail-closed: if we can't parse, return null and let
97
110
  // the policy match against {} (which fails any specific clause).
98
- function safeParseToolArgs(rawArgs) {
111
+ //
112
+ // v1.4 F-32 — cap the input bytes before JSON.parse. Beyond the cap
113
+ // the input is truncated with a sentinel and a parsed shape that
114
+ // preserves the field structure as much as possible (try-parse the
115
+ // truncated text, fall back to `{ _wmaTruncated: true, original_bytes }`).
116
+ // Policies that match on tool_name still work; policies that match on
117
+ // argument values silently miss the truncated suffix — which is the
118
+ // correct fail-closed behavior for an oversize input.
119
+ function safeParseToolArgs(rawArgs, maxBytes = DEFAULT_MAX_ARG_BYTES) {
99
120
  if (rawArgs == null) return null;
100
- if (typeof rawArgs === 'object') return rawArgs;
121
+ if (typeof rawArgs === 'object') {
122
+ // Already parsed by the SDK or the customer. We don't deep-walk
123
+ // and truncate — that would silently change policy match semantics
124
+ // on nested fields. Defer the decision to the caller; the size cap
125
+ // applies at the string-parse boundary, which is the realistic
126
+ // SDK entry point.
127
+ return rawArgs;
128
+ }
101
129
  if (typeof rawArgs !== 'string') return null;
130
+ // Byte cap: Buffer.byteLength is the safe length on the wire.
131
+ const byteLen = Buffer.byteLength(rawArgs, 'utf8');
132
+ if (byteLen > maxBytes) {
133
+ // Truncate to maxBytes worth of bytes (substring is char-bounded;
134
+ // good enough — exact byte truncation isn't required since we mark
135
+ // the result as truncated and never feed it back to a real tool).
136
+ const head = rawArgs.slice(0, maxBytes);
137
+ try {
138
+ const parsed = JSON.parse(head);
139
+ // If by luck the head is valid JSON, return it plus the marker.
140
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
141
+ return { ...parsed, _wmaTruncated: true, _wmaOriginalBytes: byteLen };
142
+ }
143
+ return { _wmaTruncated: true, _wmaOriginalBytes: byteLen, head: parsed };
144
+ } catch {
145
+ // Common path: truncated JSON is invalid mid-tree.
146
+ return { _wmaTruncated: true, _wmaOriginalBytes: byteLen };
147
+ }
148
+ }
102
149
  try { return JSON.parse(rawArgs); }
103
150
  catch { return null; }
104
151
  }
@@ -110,6 +157,17 @@ function makeSessionId() {
110
157
  return `oai-${randomUUID()}`;
111
158
  }
112
159
 
160
+ // v1.4 F-31 — short reference code minted per Shield internal error.
161
+ // Returned to the model as `Ref: WMA-SHL-<8hex>` and logged to stderr
162
+ // alongside the full err.message so an operator can correlate the
163
+ // generic model-facing message to the local detailed log without
164
+ // leaking the raw error to the model.
165
+ function makeErrorRef() {
166
+ // 8 hex chars from a fresh UUID — wide enough to deconflict within
167
+ // a session, short enough to be readable in a tool result.
168
+ return `WMA-SHL-${randomUUID().replace(/-/g, '').slice(0, 8)}`;
169
+ }
170
+
113
171
  // Derive a stable, monotonic event id from the SDK's tool_call id when
114
172
  // present (so deduplication after restart works), else mint a UUID.
115
173
  function makeEventId(toolCall) {
@@ -274,8 +332,18 @@ export function normalizeAgentHandoff({ fromAgent, toAgent, sessionId, teamId })
274
332
  });
275
333
  }
276
334
 
277
- export function normalizeToolStart({ agent, tool, toolCall, sessionId, teamId }) {
335
+ export function normalizeToolStart({ agent, tool, toolCall, sessionId, teamId, toolInputs }) {
278
336
  const parsedArgs = safeParseToolArgs(toolCall?.arguments);
337
+ // v1.4 Codex #4 — per-tool argument aliases. If `toolInputs` is
338
+ // provided AND has an entry for this tool's name, apply the alias
339
+ // map via normalizeToolInput() from the anonymizer. The result
340
+ // populates canonical names (`url`, `query`, `command`, `path`,
341
+ // `file_path`) so the SignalsAggregator hashes them EXACTLY, not
342
+ // via the F-30 suffix heuristic (which is the opt-out safety net,
343
+ // not the precision path). Both layers coexist: explicit aliases
344
+ // win on declared fields; heuristic catches the long tail.
345
+ const aliases = toolInputs && tool?.name ? toolInputs[tool.name] : null;
346
+ const finalInput = aliases ? normalizeToolInput(parsedArgs, aliases) : parsedArgs;
279
347
  return Object.freeze({
280
348
  id: makeEventId(toolCall),
281
349
  provider: PROVIDER,
@@ -292,11 +360,44 @@ export function normalizeToolStart({ agent, tool, toolCall, sessionId, teamId })
292
360
  parent_agent_id: null,
293
361
  composition_pattern: COMPOSITION_PATTERNS.SOLO,
294
362
  team_id: teamId,
295
- input: parsedArgs,
363
+ input: finalInput,
296
364
  output: null,
297
365
  });
298
366
  }
299
367
 
368
+ // v1.4 F-32 — cap result bytes before writing NDJSON. Verbose tools
369
+ // (HTML scrapers, web_fetch with full-page payloads, computer use
370
+ // screenshots base64'd) can return arbitrary megabytes. Without a cap
371
+ // we'd write a single ~MB-sized JSON line per call into the rotation
372
+ // file, fill the disk, and slow every subsequent NDJSON read.
373
+ function truncateResult(result, maxBytes = DEFAULT_MAX_RESULT_BYTES) {
374
+ if (typeof result === 'string') {
375
+ const byteLen = Buffer.byteLength(result, 'utf8');
376
+ if (byteLen > maxBytes) {
377
+ return {
378
+ text: result.slice(0, maxBytes) + TRUNCATION_SENTINEL,
379
+ _wmaTruncated: true,
380
+ _wmaOriginalBytes: byteLen,
381
+ };
382
+ }
383
+ return { text: result };
384
+ }
385
+ if (result == null) return { value: null };
386
+ // Object / array result: serialize, check size, truncate if needed.
387
+ try {
388
+ const serialized = JSON.stringify(result);
389
+ const byteLen = Buffer.byteLength(serialized, 'utf8');
390
+ if (byteLen > maxBytes) {
391
+ return {
392
+ value: { _wmaTruncated: true, _wmaOriginalBytes: byteLen },
393
+ };
394
+ }
395
+ return { value: result };
396
+ } catch {
397
+ return { value: { _wmaTruncated: true, _wmaUnserializable: true } };
398
+ }
399
+ }
400
+
300
401
  export function normalizeToolEnd({ agent, tool, result, toolCall, sessionId, teamId }) {
301
402
  return Object.freeze({
302
403
  id: `${makeEventId(toolCall)}-end`,
@@ -315,7 +416,7 @@ export function normalizeToolEnd({ agent, tool, result, toolCall, sessionId, tea
315
416
  composition_pattern: COMPOSITION_PATTERNS.SOLO,
316
417
  team_id: teamId,
317
418
  input: null,
318
- output: typeof result === 'string' ? { text: result } : { value: result },
419
+ output: truncateResult(result),
319
420
  });
320
421
  }
321
422
 
@@ -354,8 +455,13 @@ export function wmaToolInputGuardrail(options = {}) {
354
455
  let ruleset = options.ruleset || null;
355
456
  const tracker = options.tracker
356
457
  || createContextTracker({ recentWindowSize: options.recentWindowSize ?? 20 });
458
+ // v1.3.1 F-29 (P1 Codex audit): provider must be passed explicitly so
459
+ // shield_decision NDJSON rows carry 'openai-agents' instead of being
460
+ // mis-attributed to 'anthropic-managed' (DecisionLogger's pre-v1.3.1
461
+ // hardcoded default). Fortress / Guardian forensic surfaces depend on
462
+ // this for correct multi-provider attribution.
357
463
  const decisionLogger = options.decisionLogger
358
- || new DecisionLogger({ logDir, agentId: 'openai-agents', sessionId });
464
+ || new DecisionLogger({ logDir, agentId: 'openai-agents', sessionId, provider: PROVIDER });
359
465
  const logger = options.logger
360
466
  || new Logger({ logDir, agentId: 'openai-agents', sessionId, silent: true, bestEffort: false });
361
467
 
@@ -406,6 +512,10 @@ export function wmaToolInputGuardrail(options = {}) {
406
512
  toolCall,
407
513
  sessionId,
408
514
  teamId: getTeamId(),
515
+ // v1.4 Codex #4 — per-tool alias map (option `toolInputs`).
516
+ // Threaded through so the anonymizer hashes by canonical name
517
+ // for tools the customer explicitly mapped.
518
+ toolInputs: options.toolInputs,
409
519
  });
410
520
 
411
521
  // 2. Compute policy context BEFORE evaluation so recent_error_rate
@@ -462,17 +572,39 @@ export function wmaToolInputGuardrail(options = {}) {
462
572
  // Shield internal failure (policy file unreadable, disk full on
463
573
  // log write, etc.). Default fail-CLOSED unless customer opted
464
574
  // into fail-OPEN.
575
+ //
576
+ // v1.4 F-31 (P2 Codex audit on v1.3.0): the agent must NOT see
577
+ // the raw err.message — it can carry local paths, policy file
578
+ // names, disk-mount strings, permission codes, or even fragments
579
+ // of the policy itself that should never reach a model context.
580
+ // We mint a short reference code, log the full error locally to
581
+ // stderr keyed by that code, and return a generic message that
582
+ // operators can correlate against the local log without exposing
583
+ // anything actionable to the model. The outputInfo (which is NOT
584
+ // surfaced to the model — it stays in the application's run
585
+ // metadata) keeps the err.message for SDK-side tracing.
586
+ const errorRef = makeErrorRef();
465
587
  process.stderr.write(
466
- `[wma/openai-agents] guardrail error: ${err.message}\n`,
588
+ `[wma/openai-agents] [${errorRef}] guardrail error: ${err.message}\n`,
467
589
  );
468
590
  if (failOpen) {
469
591
  return ToolGuardrailFunctionOutputFactory.allow({
470
- wma: { error: err.message, failOpenApplied: true },
592
+ wma: {
593
+ errorRef,
594
+ error: err.message,
595
+ failOpenApplied: true,
596
+ },
471
597
  });
472
598
  }
473
599
  return ToolGuardrailFunctionOutputFactory.rejectContent(
474
- `WMA Shield error: ${err.message} (fail-closed)`,
475
- { wma: { error: err.message, failOpenApplied: false } },
600
+ `WMA Shield internal error (fail-closed). Ref: ${errorRef}`,
601
+ {
602
+ wma: {
603
+ errorRef,
604
+ error: err.message,
605
+ failOpenApplied: false,
606
+ },
607
+ },
476
608
  );
477
609
  }
478
610
  },
@@ -545,6 +677,7 @@ export function attachWmaWatch(runner, options = {}) {
545
677
  const teamId = teamTracker.resolve(sessionId) || teamTracker.bootstrap(sessionId);
546
678
  const event = normalizeToolStart({
547
679
  agent, tool, toolCall: details?.toolCall, sessionId, teamId,
680
+ toolInputs: options.toolInputs, // v1.4 Codex #4
548
681
  });
549
682
  await logger.write(event);
550
683
  } catch (e) {
@@ -669,6 +802,7 @@ export function attachWmaWatchToAgent(agent, options = {}) {
669
802
  const teamId = teamTracker.resolve(sessionId) || teamTracker.bootstrap(sessionId);
670
803
  const event = normalizeToolStart({
671
804
  agent, tool, toolCall: details?.toolCall, sessionId, teamId,
805
+ toolInputs: options.toolInputs, // v1.4 Codex #4
672
806
  });
673
807
  await logger.write(event);
674
808
  } catch (e) {
@@ -710,7 +844,12 @@ export const adapterMeta = Object.freeze({
710
844
  compositionDefault: COMPOSITION_PATTERNS.SOLO,
711
845
  customerInstrumented: true, // not auto-discovery
712
846
  peerPackage: '@openai/agents',
713
- minPeerVersion: '0.1.0', // adjust after testing
847
+ // v1.4 F-33 aligned with package.json#peerDependencies range
848
+ // `^0.2.0`. Real fixtures captured against 0.2.x verified the
849
+ // lifecycle event signatures in v1.3.0; minPeerVersion below that is
850
+ // unsupported and may show drift in args layout (the AgentHooks vs
851
+ // RunHooks gap that motivated the v1.3.0 finalize commit).
852
+ minPeerVersion: '0.2.0',
714
853
  capabilities: Object.freeze({
715
854
  watch: true,
716
855
  shield: true,