watchmyagents 1.4.7 → 1.4.9
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
|
-
|
|
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)
|
|
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 —
|
|
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.
|
|
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
|
-
|
|
|
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.
|
|
3
|
+
"version": "1.4.9",
|
|
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/
|
|
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;
|
package/src/openai-agents.js
CHANGED
|
@@ -130,6 +130,7 @@ export function openaiAgents(options = {}) {
|
|
|
130
130
|
apiKey,
|
|
131
131
|
base,
|
|
132
132
|
anthropicAgentId: options.agentId, // scopes get-policies by native agent id
|
|
133
|
+
provider: adapterMeta.provider, // v1.4.9: 'openai-agents' → Fortress scopes by (provider, native_agent_id)
|
|
133
134
|
requireSignedPolicies: options.policies.requireSignedPolicies,
|
|
134
135
|
failMode: options.policies.failMode,
|
|
135
136
|
});
|
|
@@ -163,6 +164,7 @@ export function openaiAgents(options = {}) {
|
|
|
163
164
|
agentId: options.agentId, // v1.4.6: NDJSON path + native_agent_id
|
|
164
165
|
fortressPolicySource, // v1.4.6: live Fortress ruleset (or null)
|
|
165
166
|
fortressDecisionSink, // v1.4.7 #1: ship decisions (or null)
|
|
167
|
+
maxDecisionUploadsInFlight: options.maxDecisionUploadsInFlight, // v1.4.8: backpressure cap
|
|
166
168
|
signalsSalt: options.signalsSalt || process.env.WMA_SIGNALS_SALT, // hash session/input
|
|
167
169
|
policiesPath: options.policiesPath,
|
|
168
170
|
ruleset: options.ruleset,
|
|
@@ -83,15 +83,25 @@ function httpsJson(method, url, headers, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
83
83
|
* @param {object} opts
|
|
84
84
|
* @param {string} opts.apiKey - wma_xxx
|
|
85
85
|
* @param {string} opts.base - Fortress base URL (https://x.supabase.co/functions/v1)
|
|
86
|
-
* @param {string} [opts.anthropicAgentId] - optional filter
|
|
86
|
+
* @param {string} [opts.anthropicAgentId] - optional native agent id filter
|
|
87
|
+
* @param {string} [opts.provider] - runtime provider (default 'anthropic-managed')
|
|
87
88
|
* @returns {Promise<{ ok: true, policies: array, signing_keys: array, fetched_at: string }>}
|
|
88
89
|
*/
|
|
89
|
-
|
|
90
|
+
// v1.4.9: pure, testable get-policies URL builder. Always carries `provider`
|
|
91
|
+
// so a provider-aware Fortress can scope by (provider, native_agent_id) — an
|
|
92
|
+
// OpenAI agent id is NOT in the `agent_…` shape. Harmless on the older Fortress
|
|
93
|
+
// that ignores unknown query params.
|
|
94
|
+
export function buildGetPoliciesUrl(base, { anthropicAgentId, provider = 'anthropic-managed' } = {}) {
|
|
90
95
|
let url = fortressEndpoint(base, 'get-policies');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const params = [];
|
|
97
|
+
if (anthropicAgentId) params.push(`agent_id=${encodeURIComponent(anthropicAgentId)}`);
|
|
98
|
+
params.push(`provider=${encodeURIComponent(provider)}`);
|
|
99
|
+
url += (url.includes('?') ? '&' : '?') + params.join('&');
|
|
100
|
+
return url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function fetchPolicies({ apiKey, base, anthropicAgentId, provider = 'anthropic-managed' }) {
|
|
104
|
+
const url = buildGetPoliciesUrl(base, { anthropicAgentId, provider });
|
|
95
105
|
const { status, body } = await httpsJson('GET', url, {
|
|
96
106
|
authorization: `Bearer ${apiKey}`,
|
|
97
107
|
accept: 'application/json',
|
|
@@ -195,12 +205,16 @@ const ROOT_PUBLIC_KEY = (() => {
|
|
|
195
205
|
})();
|
|
196
206
|
|
|
197
207
|
export class FortressPolicySource {
|
|
198
|
-
constructor({ apiKey, base, anthropicAgentId, refreshIntervalMs = 5 * 60_000, onError, onRefresh, requireSignedPolicies, failMode }) {
|
|
208
|
+
constructor({ apiKey, base, anthropicAgentId, provider = 'anthropic-managed', refreshIntervalMs = 5 * 60_000, onError, onRefresh, requireSignedPolicies, failMode }) {
|
|
199
209
|
if (!apiKey) throw new Error('FortressPolicySource: apiKey required');
|
|
200
210
|
if (!base) throw new Error('FortressPolicySource: base URL required');
|
|
201
211
|
this.apiKey = apiKey;
|
|
202
212
|
this.base = base;
|
|
203
213
|
this.anthropicAgentId = anthropicAgentId;
|
|
214
|
+
// v1.4.9: runtime provider, sent to get-policies so Fortress scopes by
|
|
215
|
+
// (provider, native_agent_id). Defaults to anthropic-managed (the Anthropic
|
|
216
|
+
// shield path doesn't pass it); the OpenAI factory passes 'openai-agents'.
|
|
217
|
+
this.provider = provider;
|
|
204
218
|
this.refreshIntervalMs = refreshIntervalMs;
|
|
205
219
|
this.onError = onError || (() => {});
|
|
206
220
|
this.onRefresh = onRefresh || (() => {});
|
|
@@ -257,6 +271,7 @@ export class FortressPolicySource {
|
|
|
257
271
|
apiKey: this.apiKey,
|
|
258
272
|
base: this.base,
|
|
259
273
|
anthropicAgentId: this.anthropicAgentId,
|
|
274
|
+
provider: this.provider,
|
|
260
275
|
});
|
|
261
276
|
}
|
|
262
277
|
|
|
@@ -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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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).
|