watchmyagents 1.4.6 → 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
|
-
|
|
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.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/
|
|
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
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
attachWmaWatchToAgent,
|
|
41
41
|
adapterMeta,
|
|
42
42
|
} from './sources/openai-agents-js.js';
|
|
43
|
-
import { FortressPolicySource } from './shield/sources/fortress.js';
|
|
43
|
+
import { FortressPolicySource, postDecision } from './shield/sources/fortress.js';
|
|
44
44
|
import { resolveFortressBase } from './fortress/url.js';
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -101,6 +101,7 @@ export function openaiAgents(options = {}) {
|
|
|
101
101
|
// wma-shield gets its ruleset for Anthropic. Requires an agentId to scope
|
|
102
102
|
// the pull, a base URL (option or WMA_FORTRESS_BASE_URL), and WMA_API_KEY.
|
|
103
103
|
let fortressPolicySource = null;
|
|
104
|
+
let fortressDecisionSink = null;
|
|
104
105
|
if (options.policies && options.policies.source === 'fortress') {
|
|
105
106
|
if (!options.agentId) {
|
|
106
107
|
throw new Error(
|
|
@@ -132,6 +133,14 @@ export function openaiAgents(options = {}) {
|
|
|
132
133
|
requireSignedPolicies: options.policies.requireSignedPolicies,
|
|
133
134
|
failMode: options.policies.failMode,
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
// v1.4.7 (#1): ship decisions to the SAME Fortress (the control plane you
|
|
138
|
+
// pull policies from is where the decisions belong). Auto-on; opt out with
|
|
139
|
+
// policies.uploadDecisions === false to keep decisions local-only. The
|
|
140
|
+
// guardrail fires these best-effort, off its hot path.
|
|
141
|
+
if (options.policies.uploadDecisions !== false) {
|
|
142
|
+
fortressDecisionSink = (decision) => postDecision({ apiKey, base, decision });
|
|
143
|
+
}
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
// v1.4 Codex #2 — fail-loud refusal to start in enforce mode without
|
|
@@ -153,6 +162,9 @@ export function openaiAgents(options = {}) {
|
|
|
153
162
|
const sharedOptions = {
|
|
154
163
|
agentId: options.agentId, // v1.4.6: NDJSON path + native_agent_id
|
|
155
164
|
fortressPolicySource, // v1.4.6: live Fortress ruleset (or null)
|
|
165
|
+
fortressDecisionSink, // v1.4.7 #1: ship decisions (or null)
|
|
166
|
+
maxDecisionUploadsInFlight: options.maxDecisionUploadsInFlight, // v1.4.8: backpressure cap
|
|
167
|
+
signalsSalt: options.signalsSalt || process.env.WMA_SIGNALS_SALT, // hash session/input
|
|
156
168
|
policiesPath: options.policiesPath,
|
|
157
169
|
ruleset: options.ruleset,
|
|
158
170
|
logDir: options.logDir,
|
|
@@ -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
|
/**
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import { evaluate, loadPolicies } from '../shield/policy.js';
|
|
55
55
|
import { createContextTracker } from '../shield/context.js';
|
|
56
56
|
import { DecisionLogger } from '../shield/decisions.js';
|
|
57
|
+
import { buildFortressDecisionPayload } from '../shield/upload.js';
|
|
57
58
|
import { Logger } from '../logger.js';
|
|
58
59
|
import { normalizeToolInput } from '../anonymizer.js';
|
|
59
60
|
|
|
@@ -522,6 +523,15 @@ export function wmaToolInputGuardrail(options = {}) {
|
|
|
522
523
|
? options.getTeamId
|
|
523
524
|
: () => teamTracker.bootstrap(sessionId);
|
|
524
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
|
+
|
|
525
535
|
// Lazily load the ruleset on first invocation if a path was given.
|
|
526
536
|
// v1.4.6: SINGLE-FLIGHT start. start() creates a setInterval, so two
|
|
527
537
|
// concurrent first tool-calls must NOT both call it (that leaks a second
|
|
@@ -632,6 +642,55 @@ export function wmaToolInputGuardrail(options = {}) {
|
|
|
632
642
|
enforcementDelivered: enforcedBlock ? true : undefined,
|
|
633
643
|
});
|
|
634
644
|
|
|
645
|
+
// 4b. v1.4.7 (#1): ship the decision to Fortress's ingest-decisions when
|
|
646
|
+
// a sink is configured. FIRE-AND-FORGET, off the guardrail's hot path —
|
|
647
|
+
// identical model to the Anthropic shield's fireToFortress(). The NDJSON
|
|
648
|
+
// row above is the durable local record; this live post is best-effort
|
|
649
|
+
// (a failure is swallowed, never blocks the tool call or crashes the
|
|
650
|
+
// guardrail). Payload is anonymized (hashes + decision/rule/provider) so
|
|
651
|
+
// Containment holds. Carries provider='openai-agents' + native_agent_id +
|
|
652
|
+
// enforcement_delivered (the v1.4.6 prereqs) so Fortress attributes it
|
|
653
|
+
// to the right runtime and surfaces degraded enforcement.
|
|
654
|
+
if (options.fortressDecisionSink) {
|
|
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
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
635
694
|
// 5. Watch log (the event itself, separate from the decision).
|
|
636
695
|
await logger.write(event);
|
|
637
696
|
|