watchmyagents 1.4.7 → 1.4.8

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.
@@ -29,12 +29,31 @@ Sign in to the Fortress dashboard → **Settings → API Keys → Generate** →
29
29
  ```bash
30
30
  # .env
31
31
  WMA_API_KEY=wma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
32
+ WMA_FORTRESS_BASE_URL=https://<project>.supabase.co/functions/v1 # for Fortress policies + decisions
33
+ WMA_SIGNALS_SALT=<stable per-customer salt> # hashes session/input in uploads
32
34
  WMA_LOG_DIR=./watchmyagents-logs # optional; default shown
33
35
  WMA_TEAM_ID=customer-support # optional; manual team tagging
34
- # WMA_POLICIES_SOURCE handling for Fortress-pulled policies arrives in 1.3.1
35
36
  ```
36
37
 
37
- Today the adapter loads policies from a local file via `policiesPath`. Fortress-pulled policies for this adapter land in a 1.3.x patch.
38
+ **Policy source (v1.4.6+).** The adapter can load policies two ways:
39
+ - **Fortress (recommended)** — `policies: { source: 'fortress' }` pulls the ruleset
40
+ live from Fortress (the same control plane `wma-shield` uses for Anthropic) and
41
+ refreshes it in the background. Requires `agentId` + `WMA_API_KEY` +
42
+ `WMA_FORTRESS_BASE_URL`. This is what the Fortress "Register an OpenAI agent"
43
+ onboarding gives you.
44
+ - **Local file** — `policiesPath: './policies.json'` (or an in-memory `ruleset`),
45
+ for tests / air-gapped setups.
46
+
47
+ ```typescript
48
+ import { openaiAgents } from 'watchmyagents/openai-agents';
49
+
50
+ const wma = openaiAgents({
51
+ mode: 'enforce',
52
+ agentId: 'support_bot', // your Fortress-registered agent id
53
+ policies: { source: 'fortress' }, // WMA_API_KEY + WMA_FORTRESS_BASE_URL from env
54
+ // policies: { source: 'fortress', uploadDecisions: false } // ← keep decisions local-only
55
+ });
56
+ ```
38
57
 
39
58
  ### 4. Wire it up — two patterns
40
59
 
@@ -190,20 +209,23 @@ attachWmaWatch(runner, {
190
209
 
191
210
  ## What gets sent to Fortress
192
211
 
193
- Tool arguments and results stay LOCAL. The anonymizer converts WMAAction → signals payload (salted hashes, no raw values) and pushes to `WMA_FORTRESS_BASE_URL/upload-signals` if `WMA_API_KEY` is set. See [CONTAINMENT.md](../CONTAINMENT.md) for the full egress model.
212
+ Tool arguments and results stay LOCAL. The anonymizer converts WMAAction → a signals payload (salted hashes, no raw values). Two egress paths, both anonymized see [CONTAINMENT.md](../CONTAINMENT.md) for the full model:
213
+
214
+ - **Signals** — aggregated counts + salted IoC hashes, uploaded to `ingest-signals` (run `wma-upload-fortress --provider openai-agents --agent-id <id>` against the NDJSON, or the Watch path).
215
+ - **Decisions (v1.4.7+)** — when `policies: { source: 'fortress' }` is set, the guardrail ships each Shield decision to `ingest-decisions` **fire-and-forget** (off the hot path). The payload carries `provider: 'openai-agents'`, `native_agent_id`, the decision/rule, salted `session_hash`/`event_id_hash`/`input_hash`, and `enforcement_delivered` (always `true` for an enforced deny/interrupt — the in-process guardrail blocks synchronously). The local NDJSON `shield_decision` row is the durable record; a failed upload never blocks the tool call. In-flight uploads are bounded (`maxDecisionUploadsInFlight`, default 32) — under a Fortress outage the excess is dropped from the live post (still in NDJSON). Opt out with `policies: { uploadDecisions: false }` to keep decisions local-only.
194
216
 
195
217
  ## Compatibility
196
218
 
197
219
  - Node.js: **20+** (matches the WMA SDK baseline and `@openai/agents`'s requirement)
198
- - TypeScript: any version — types live in `src/sources/openai-agents-js.d.ts`
220
+ - TypeScript: any version — `import { openaiAgents } from 'watchmyagents/openai-agents'` is fully typed (`src/openai-agents.d.ts`); the low-level escape hatches are typed in `src/sources/openai-agents-js.d.ts`.
199
221
  - `@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
222
 
201
- ## Limits + known gaps (v1.3.0)
223
+ ## Limits + known gaps (v1.4.8)
202
224
 
203
225
  | Gap | Workaround |
204
226
  |---|---|
205
227
  | Streaming mode behavior under Tool Input Guardrails not formally verified | We expect the SDK to honor guardrails in streaming mode (the SDK code paths are the same); the test fixtures cover non-streaming today. Verification with a streaming smoke test before v1.3.1. |
206
- | Fortress-pulled policies via `WMA_POLICIES_SOURCE=fortress` not wired in this adapter yet | Use `policiesPath` with a local file in v1.3.0; Fortress pull lands in v1.3.x patch |
228
+ | Decision uploads are live fire-and-forget a short-lived process can lose in-flight posts on exit | The NDJSON `shield_decision` row is durable; a batch backfill (NDJSON `ingest-decisions`) is planned for fully-durable short runs |
207
229
  | Tool Output Guardrails (post-execution filter) not exposed | Add only if customer demand — Shield's pre-tool deny covers 95% of the exfil-prevention use case |
208
230
  | Python Agents SDK | Separate package `watchmyagents-py` planned for v1.4.0 (Pattern 2 — pip-installable thin client that talks to a Node Shield daemon over UNIX socket) |
209
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchmyagents",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
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": [
@@ -22,7 +22,7 @@
22
22
  "exports": {
23
23
  ".": "./src/index.js",
24
24
  "./openai-agents": {
25
- "types": "./src/sources/openai-agents-js.d.ts",
25
+ "types": "./src/openai-agents.d.ts",
26
26
  "default": "./src/openai-agents.js"
27
27
  },
28
28
  "./package.json": "./package.json"
@@ -0,0 +1,110 @@
1
+ // Type declarations for the stable entry point `watchmyagents/openai-agents`.
2
+ //
3
+ // The runtime is src/openai-agents.js (the openaiAgents() factory + the
4
+ // re-exported low-level escape hatches). v1.4.8 (P2 quality): these types were
5
+ // previously pointed at the low-level module's .d.ts, which never declared
6
+ // openaiAgents() nor the v1.4.6/1.4.7 options (agentId, policies.source,
7
+ // uploadDecisions, signalsSalt) — TS users got a broken/incomplete typed API.
8
+
9
+ import type {
10
+ WMAToolInputGuardrail,
11
+ WMAGuardrailOptions,
12
+ WMAAdapterMeta,
13
+ WMARunnerLike,
14
+ WMAAgentLike,
15
+ WMAWatchOptions,
16
+ } from './sources/openai-agents-js';
17
+
18
+ // Re-export the low-level types + escape-hatch functions so TS users importing
19
+ // from 'watchmyagents/openai-agents' see the full surface the runtime exports.
20
+ export * from './sources/openai-agents-js';
21
+
22
+ /** Live policy source config. Today only Fortress is supported. */
23
+ export interface OpenaiAgentsPoliciesConfig {
24
+ /** Pull the ruleset from Fortress (the same control plane wma-shield uses). */
25
+ source: 'fortress';
26
+ /** Fortress functions base URL. Defaults to WMA_FORTRESS_BASE_URL. */
27
+ baseUrl?: string;
28
+ /** WMA API key. Defaults to WMA_API_KEY. */
29
+ apiKey?: string;
30
+ /** Require Ed25519-signed policies (strict mode). */
31
+ requireSignedPolicies?: boolean;
32
+ /** Fail mode when a refresh yields no valid policies ('open' | 'closed'). */
33
+ failMode?: 'open' | 'closed';
34
+ /** Ship Shield decisions to the same Fortress's ingest-decisions.
35
+ * Default: true. Set false to keep decisions local-only (NDJSON). */
36
+ uploadDecisions?: boolean;
37
+ }
38
+
39
+ /** Options for {@link openaiAgents}. */
40
+ export interface OpenaiAgentsOptions {
41
+ /** 'enforce' (default) requires a policy source; 'observe' attaches Watch
42
+ * only and makes .shield() throw. */
43
+ mode?: 'observe' | 'enforce';
44
+ /** Agent id registered in Fortress — NDJSON path + native_agent_id, and
45
+ * scopes the Fortress policy pull. Required with policies.source='fortress'. */
46
+ agentId?: string;
47
+ /** Live policy source (Fortress). Mutually complete with policiesPath/ruleset. */
48
+ policies?: OpenaiAgentsPoliciesConfig;
49
+ /** Local JSON policy file. */
50
+ policiesPath?: string;
51
+ /** In-memory ruleset (overrides policiesPath). */
52
+ ruleset?: { policies: unknown[]; default?: { action: string } };
53
+ /** NDJSON log dir. Defaults to WMA_LOG_DIR or ./watchmyagents-logs. */
54
+ logDir?: string;
55
+ /** Override the session id (default: auto-minted). */
56
+ sessionId?: string;
57
+ /** Salt for hashing session/input in the Fortress decision payload.
58
+ * Defaults to WMA_SIGNALS_SALT. */
59
+ signalsSalt?: string;
60
+ /** On Shield internal error, allow vs deny. Default: false (fail-CLOSED). */
61
+ failOpen?: boolean;
62
+ /** Trailing window for ctx.recent_error_rate etc. Default: 20. */
63
+ recentWindowSize?: number;
64
+ /** Custom team id resolver. */
65
+ getTeamId?: () => string | null;
66
+ /** Per-tool argument aliases ({ tool: { nativeField: canonical } }). */
67
+ toolInputs?: Record<string, Record<string, string>>;
68
+ /** Byte caps on captured tool args / results. Default 256 KB. */
69
+ maxArgBytes?: number;
70
+ maxResultBytes?: number;
71
+ /** Cap on concurrent in-flight decision uploads (excess dropped best-effort,
72
+ * NDJSON stays durable). Default: 32. */
73
+ maxDecisionUploadsInFlight?: number;
74
+ /** Inject for testing. */
75
+ tracker?: unknown;
76
+ logger?: unknown;
77
+ decisionLogger?: unknown;
78
+ }
79
+
80
+ /** The object returned by {@link openaiAgents}. */
81
+ export interface OpenaiAgentsHandle {
82
+ /** The @openai/agents Tool Input Guardrail (Shield). Throws in observe mode. */
83
+ shield(): WMAToolInputGuardrail;
84
+ /** Attach Watch — auto-detects Runner vs Agent. Returns a detach function. */
85
+ watch(target: WMARunnerLike | WMAAgentLike): () => void;
86
+ /** Adapter metadata. */
87
+ readonly meta: WMAAdapterMeta;
88
+ /** The configured mode. */
89
+ readonly mode: 'observe' | 'enforce';
90
+ }
91
+
92
+ /**
93
+ * Build the WMA OpenAI Agents SDK adapter from one shared config.
94
+ *
95
+ * ```typescript
96
+ * import { openaiAgents } from 'watchmyagents/openai-agents';
97
+ * import { Agent, run } from '@openai/agents';
98
+ *
99
+ * const wma = openaiAgents({
100
+ * mode: 'enforce',
101
+ * agentId: 'support_bot',
102
+ * policies: { source: 'fortress' }, // WMA_API_KEY + WMA_FORTRESS_BASE_URL from env
103
+ * });
104
+ *
105
+ * const agent = new Agent({ name: 'support_bot', tools, toolInputGuardrails: [wma.shield()] });
106
+ * wma.watch(agent);
107
+ * await run(agent, 'help me');
108
+ * ```
109
+ */
110
+ export function openaiAgents(options?: OpenaiAgentsOptions): OpenaiAgentsHandle;
@@ -163,6 +163,7 @@ export function openaiAgents(options = {}) {
163
163
  agentId: options.agentId, // v1.4.6: NDJSON path + native_agent_id
164
164
  fortressPolicySource, // v1.4.6: live Fortress ruleset (or null)
165
165
  fortressDecisionSink, // v1.4.7 #1: ship decisions (or null)
166
+ maxDecisionUploadsInFlight: options.maxDecisionUploadsInFlight, // v1.4.8: backpressure cap
166
167
  signalsSalt: options.signalsSalt || process.env.WMA_SIGNALS_SALT, // hash session/input
167
168
  policiesPath: options.policiesPath,
168
169
  ruleset: options.ruleset,
@@ -110,6 +110,32 @@ export interface WMAGuardrailOptions {
110
110
  decisionLogger?: WMADecisionLogger;
111
111
  /** Inject an existing ContextTracker (testing). */
112
112
  tracker?: WMAContextTracker;
113
+ /** v1.4.6 — agent id used for the NDJSON path segment + native_agent_id.
114
+ * Defaults to 'openai-agents' when un-named. */
115
+ agentId?: string;
116
+ /** v1.4.6 — a live policy source (e.g. FortressPolicySource). The guardrail
117
+ * single-flight-starts it then reads current() on every call. */
118
+ fortressPolicySource?: {
119
+ start(): Promise<void>;
120
+ current(): { policies: unknown[]; default?: { action: string } };
121
+ };
122
+ /** v1.4.7 — best-effort sink the guardrail fires each decision to
123
+ * (fire-and-forget). Built by openaiAgents() from policies.source='fortress'. */
124
+ fortressDecisionSink?: (payload: object) => unknown | Promise<unknown>;
125
+ /** v1.4.7 — salt for hashing session/input in the Fortress decision payload
126
+ * (defaults to WMA_SIGNALS_SALT). Without it, those hashes are null. */
127
+ signalsSalt?: string;
128
+ /** v1.4.7 — cap on concurrent in-flight decision uploads; excess are dropped
129
+ * best-effort (NDJSON stays durable). Default: 32. */
130
+ maxDecisionUploadsInFlight?: number;
131
+ /** v1.4 Codex #4 — per-tool argument aliases ({ tool: { nativeField: canonical } }). */
132
+ toolInputs?: Record<string, Record<string, string>>;
133
+ /** v1.4 F-32 — byte caps on captured tool args / results. Default 256 KB. */
134
+ maxArgBytes?: number;
135
+ maxResultBytes?: number;
136
+ /** v1.4.1 F-35 — explicit escape hatch to allow ALL tool calls when no policy
137
+ * source is configured (demos/smoke tests only). */
138
+ allowAllWhenUnconfigured?: boolean;
113
139
  }
114
140
 
115
141
  /**
@@ -523,6 +523,15 @@ export function wmaToolInputGuardrail(options = {}) {
523
523
  ? options.getTeamId
524
524
  : () => teamTracker.bootstrap(sessionId);
525
525
 
526
+ // v1.4.8 (P2 audit): bound the best-effort decision uploads. A Fortress
527
+ // outage + a high tool-call rate could otherwise accumulate unbounded
528
+ // in-flight POSTs in the agent process. We cap concurrency and DROP excess
529
+ // (the NDJSON shield_decision row is the durable record), with a throttled
530
+ // stderr summary so the operator notices sustained backpressure.
531
+ const maxDecisionUploadsInFlight = options.maxDecisionUploadsInFlight ?? 32;
532
+ let decisionUploadsInFlight = 0;
533
+ let decisionUploadsDropped = 0;
534
+
526
535
  // Lazily load the ruleset on first invocation if a path was given.
527
536
  // v1.4.6: SINGLE-FLIGHT start. start() creates a setInterval, so two
528
537
  // concurrent first tool-calls must NOT both call it (that leaks a second
@@ -643,21 +652,43 @@ export function wmaToolInputGuardrail(options = {}) {
643
652
  // enforcement_delivered (the v1.4.6 prereqs) so Fortress attributes it
644
653
  // to the right runtime and surfaces degraded enforcement.
645
654
  if (options.fortressDecisionSink) {
646
- try {
647
- const payload = buildFortressDecisionPayload({
648
- agentId: loggerAgentId,
649
- provider: PROVIDER,
650
- nativeAgentId: loggerAgentId,
651
- sessionId,
652
- rawEvent: event,
653
- normalized: event,
654
- result,
655
- decidedInMs,
656
- signalsSalt: options.signalsSalt,
657
- enforcementDelivered: enforcedBlock ? true : undefined,
658
- });
659
- Promise.resolve(options.fortressDecisionSink(payload)).catch(() => undefined);
660
- } catch { /* payload build must never break enforcement */ }
655
+ if (decisionUploadsInFlight >= maxDecisionUploadsInFlight) {
656
+ // Backpressure: drop the live post (NDJSON keeps the record).
657
+ decisionUploadsDropped++;
658
+ if (decisionUploadsDropped === 1 || decisionUploadsDropped % 100 === 0) {
659
+ process.stderr.write(
660
+ `[wma/openai-agents] Fortress decision-upload backpressure: ${decisionUploadsDropped} ` +
661
+ `decision(s) dropped from the live post (still in local NDJSON); ` +
662
+ `in-flight cap=${maxDecisionUploadsInFlight}\n`,
663
+ );
664
+ }
665
+ } else {
666
+ let payload = null;
667
+ try {
668
+ payload = buildFortressDecisionPayload({
669
+ agentId: loggerAgentId,
670
+ provider: PROVIDER,
671
+ nativeAgentId: loggerAgentId,
672
+ sessionId,
673
+ rawEvent: event,
674
+ normalized: event,
675
+ result,
676
+ decidedInMs,
677
+ signalsSalt: options.signalsSalt,
678
+ enforcementDelivered: enforcedBlock ? true : undefined,
679
+ });
680
+ } catch { /* build error → skip the post; never break enforcement */ }
681
+ if (payload) {
682
+ decisionUploadsInFlight++;
683
+ // Defer the sink call INTO the promise chain so a synchronous
684
+ // throw becomes a rejection — the .finally then always runs and
685
+ // the in-flight counter can never leak (which would wedge the cap).
686
+ Promise.resolve()
687
+ .then(() => options.fortressDecisionSink(payload))
688
+ .catch(() => undefined)
689
+ .finally(() => { decisionUploadsInFlight--; });
690
+ }
691
+ }
661
692
  }
662
693
 
663
694
  // 5. Watch log (the event itself, separate from the decision).