@govplane/runtime-sdk 0.2.4 → 0.5.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/README.md CHANGED
@@ -8,15 +8,46 @@ It is intended for backend services, workers, gateways, and critical paths that
8
8
 
9
9
  ---
10
10
 
11
+ ## Architecture
12
+
13
+ ```
14
+ ┌─────────────────────────────────────────────────────────┐
15
+ │ Your Application │
16
+ │ │
17
+ │ client.evaluate({ target, context }) │
18
+ │ │ │
19
+ │ ▼ │
20
+ │ ┌─────────────────┐ bundle cache ┌───────────────┐ │
21
+ │ │ PolicyEngine │◄─────────────────│ RuntimeClient │ │
22
+ │ │ (in-process) │ │ (polling) │ │
23
+ │ └────────┬────────┘ └──────┬────────┘ │
24
+ │ │ Decision │ HTTP │
25
+ │ ▼ ▼ │
26
+ │ allow / deny / Govplane Control Plane │
27
+ │ throttle / kill_switch / (bundle endpoint only) │
28
+ │ custom │
29
+ └─────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ | Property | Details |
33
+ |---|---|
34
+ | Decision latency | < 1 ms (in-process evaluation) |
35
+ | Network dependency | Bundle fetch only — evaluation is offline-safe |
36
+ | PII in traces | None — context is never included in trace events |
37
+ | Fail-safe | Deny-by-default when bundle is missing or invalid |
38
+ | Bundle delivery | Server-compiled JSON bundle, not evaluated server-side |
39
+
40
+ ---
41
+
11
42
  ## Design Principles
12
43
 
13
- - 🔒 **No exposed endpoints** – no middleware, no inbound surface
14
- - 🧠 **Local-first evaluation** – decisions are made in-process
15
- - 🧼 **Zero PII by design** – strict context validation and allowlists
16
- - 📦 **Precompiled policies** – no DSL or dynamic code at runtime
17
- - 🧩 **Deterministic outcomes** – same input, same decision
18
- - **Cheap polling** – HEAD + ETag + conditional GET
19
- - 🧯 **Failure-safe** – backoff, degraded mode, deny-by-default
44
+ - **No exposed endpoints** – no middleware, no inbound surface
45
+ - **Local-first evaluation** – decisions are made in-process
46
+ - **Zero PII by design** – strict context validation and heuristic PII key blocking
47
+ - **Precompiled policies** – no DSL or dynamic code at runtime
48
+ - **Deterministic outcomes** – same input, same decision
49
+ - **Cheap polling** – HEAD + ETag + conditional GET
50
+ - **Failure-safe** – backoff, degraded mode, deny-by-default
20
51
 
21
52
  ---
22
53
 
@@ -25,51 +56,46 @@ It is intended for backend services, workers, gateways, and critical paths that
25
56
  ### Runtime Client
26
57
  - Efficient **HEAD-first polling** using `ETag` and `If-None-Match`
27
58
  - In-memory bundle cache
28
- - Automatic request de-duplication
59
+ - `warmStart()` to block until first bundle is ready
29
60
  - Configurable polling interval
30
61
  - **Burst mode** for incident response
31
62
  - **Exponential backoff with jitter**
32
- - Automatic **degraded state**
33
- - Status subscriptions (`ok` / `degraded`)
34
-
35
- ### Policy Engine (SDK-only)
36
- - Supports:
37
- - `allow`
38
- - `deny`
39
- - `kill_switch`
40
- - `throttle`
41
- - **Deny-by-default**
42
- - Kill-switch always wins
43
- - Throttle selects the **most restrictive rule**
44
- - Deterministic rule ordering
45
- - Precompiled `when` AST evaluation
46
- - No dynamic evaluation or code execution
63
+ - Automatic **degraded state** with `nextRetryAt` reporting
64
+ - Status subscriptions via `onStatus()` and bundle update subscriptions via `onUpdate()`
65
+
66
+ ### Policy Engine
67
+ - Five effect types: `allow`, `deny`, `kill_switch`, `throttle`, `custom`
68
+ - **Deny-by-default** – no bundle or no match always denies
69
+ - Fixed precedence: `kill_switch > deny > throttle > allow > custom > deny-by-default`
70
+ - Throttle selects the **most restrictive** matching rule
71
+ - `thenEffect` / `elseEffect` for conditional branching within a single rule
72
+ - Policy-level `defaults` as a fallback when no rule matches
73
+ - Deterministic rule ordering by `priority`, then `policyKey`, then `ruleId`
74
+ - Precompiled `when` AST evaluation no dynamic code execution
75
+ - Optional automatic JSON parsing of `custom` effect values (`parseCustomEffect`)
47
76
 
48
77
  ### Security & Context Safety
49
- - Explicit context allowlist
50
- - Unknown keys rejected (PII protection)
51
- - Configurable limits:
52
- - max string length
53
- - max array length
54
- - max depth
55
- - Context policy is fully configurable per engine
56
-
57
- ### Decision Trace / Explain (SDK-only)
58
- - Optional decision trace generation
59
- - Compact or full trace modes
60
- - Sampling support (e.g. 1% in production)
61
- - Force tracing for debugging
62
- - Structured JSON trace output
63
- - **Async trace sinks** (fire-and-forget)
64
- - Bounded queues with drop strategies
78
+ - Explicit context `allowedKeys` allowlist — unknown keys throw immediately
79
+ - Heuristic PII key blocking (`blockLikelyPiiKeys`)
80
+ - Configurable limits: `maxStringLen`, `maxArrayLen`
81
+ - Context policy is fully configurable per engine instance
82
+ - Disable validation only in controlled test environments
83
+
84
+ ### Decision Tracing
85
+ - `evaluate()` for plain decisions; `evaluateWithTrace()` for traced decisions
86
+ - Four trace levels: `off`, `errors`, `sampled`, `full`
87
+ - Sampling rate and per-window budget controls
88
+ - `force: true` to bypass sampling for debug sessions
89
+ - Compact or full format (full includes per-rule breakdown)
90
+ - **Synchronous and async trace sinks** with bounded queues and drop strategies
91
+ - `flushTraces()` for graceful shutdown
65
92
 
66
93
  ---
67
94
 
68
95
  ## Requirements
69
96
 
70
97
  - **Node.js ≥ 18**
71
- - A valid **Govplane Runtime Key**
72
- - Access to a Govplane Runtime API (`gp-runtime`)
98
+ - A valid **Govplane Runtime Key** (`rk_live_…` or `rk_test_…`)
73
99
 
74
100
  ---
75
101
 
@@ -79,214 +105,334 @@ It is intended for backend services, workers, gateways, and critical paths that
79
105
  npm install @govplane/runtime-sdk
80
106
  ```
81
107
 
108
+ ```bash
109
+ yarn add @govplane/runtime-sdk
110
+ # or
111
+ pnpm add @govplane/runtime-sdk
112
+ ```
113
+
82
114
  ---
83
115
 
84
116
  ## Quick Start
85
117
 
86
- ### 1. Create a Runtime Client
87
-
88
- ```ts
118
+ ```typescript
89
119
  import { RuntimeClient } from "@govplane/runtime-sdk";
90
120
 
91
121
  const client = new RuntimeClient({
92
- baseUrl: "https://runtime.govplane.com",
122
+ baseUrl: "https://123456.runtime.govplane.com/",
93
123
  runtimeKey: process.env.GP_RUNTIME_KEY!,
94
- orgId: "org_dev",
95
- projectId: "prj_web",
96
- env: "prod"
97
124
  });
98
125
 
99
- client.start();
126
+ await client.warmStart(); // block until the first bundle is cached
127
+ client.start(); // begin background polling (every 5 s by default)
128
+
129
+ const result = client.evaluate({
130
+ target: { service: "payments", resource: "checkout", action: "create" },
131
+ context: { plan: "pro", country: "ES", isAuthenticated: true },
132
+ });
133
+
134
+ console.log(result.decision); // "allow" | "deny" | "throttle" | "kill_switch" | "custom"
100
135
  ```
101
136
 
102
- ### 2. Create a Policy Engine
137
+ ---
103
138
 
104
- ```ts
105
- import { createPolicyEngine } from "@govplane/runtime-sdk";
139
+ ## Effect Types
106
140
 
107
- const engine = createPolicyEngine({
108
- getBundle: () => client.getCached().bundle,
109
- validateContext: true,
110
- contextPolicy: {
111
- allowedKeys: [
112
- "ctx.plan",
113
- "ctx.country",
114
- "ctx.amount",
115
- "ctx.isAuthenticated"
116
- ],
117
- maxStringLen: 64,
118
- maxArrayLen: 10
119
- }
120
- });
141
+ The engine returns one of five decision types, applied in this fixed precedence order:
142
+
143
+ ```
144
+ kill_switch > deny > throttle > allow > custom > deny-by-default
121
145
  ```
122
146
 
123
- ### 3. Evaluate a Decision
147
+ | `decision` | When it fires |
148
+ |---------------|---------------|
149
+ | `allow` | An `allow` rule matched, or the policy default is `allow`. |
150
+ | `deny` | A `deny` rule matched, no rule matched (deny-by-default), or the policy default is `deny`. |
151
+ | `kill_switch` | A `kill_switch` rule or policy default is active. Always wins. |
152
+ | `throttle` | A `throttle` rule or policy default matched. Most restrictive wins. |
153
+ | `custom` | A `custom` rule or policy default matched — carries an arbitrary string value. |
124
154
 
125
- ```ts
126
- const result = engine.evaluate({
127
- target: {
128
- service: "payments",
129
- resource: "checkout",
130
- action: "create"
131
- },
132
- context: {
133
- plan: "pro",
134
- country: "ES",
135
- amount: 42,
136
- isAuthenticated: true
137
- }
138
- });
155
+ ---
139
156
 
140
- console.log(result.decision); // allow | deny | throttle | kill_switch
157
+ ## Decision Shape
158
+
159
+ `evaluate()` always returns a `Decision` object discriminated by `decision`:
160
+
161
+ ```typescript
162
+ type Decision =
163
+ | { decision: "allow";
164
+ reason: "default" | "rule"; policyKey?: string; ruleId?: string }
165
+ | { decision: "deny";
166
+ reason: "default" | "rule"; policyKey?: string; ruleId?: string }
167
+ | { decision: "kill_switch";
168
+ reason: "default" | "rule"; policyKey?: string; ruleId?: string;
169
+ killSwitch: { service: string; reason?: string } }
170
+ | { decision: "throttle";
171
+ reason: "default" | "rule"; policyKey?: string; ruleId?: string;
172
+ throttle: { limit: number; windowSeconds: number; key: string } }
173
+ | { decision: "custom";
174
+ reason: "default" | "rule"; policyKey?: string; ruleId?: string;
175
+ value: string; parsedValue?: unknown };
141
176
  ```
142
177
 
143
- ---
178
+ ### Handling every decision type
144
179
 
145
- ## Runtime Bundle Model
180
+ ```typescript
181
+ const result = client.evaluate({ target, context });
146
182
 
147
- The SDK retrieves **materialized runtime bundles** generated by Govplane.
183
+ if (result.decision === "allow") {
184
+ return next();
185
+ }
148
186
 
149
- ```ts
150
- type RuntimeBundleV1 = {
151
- schemaVersion: 1;
152
- orgId: string;
153
- projectId: string;
154
- env: string;
155
- generatedAt: string;
156
- policies: unknown[];
157
- };
187
+ if (result.decision === "deny") {
188
+ return reply.status(403).send({ error: "Forbidden", policy: result.policyKey });
189
+ }
190
+
191
+ if (result.decision === "throttle") {
192
+ const allowed = await rateLimiter.check(
193
+ result.throttle.key === "tenant" ? ctx.tenantId : ctx.userId,
194
+ result.throttle.limit,
195
+ result.throttle.windowSeconds,
196
+ );
197
+ if (!allowed) return reply.status(429).header("Retry-After", String(result.throttle.windowSeconds)).send();
198
+ return next();
199
+ }
200
+
201
+ if (result.decision === "kill_switch") {
202
+ return reply.status(503).send({ error: "Service Unavailable", reason: result.killSwitch.reason });
203
+ }
204
+
205
+ if (result.decision === "custom") {
206
+ const payload = result.parsedValue ?? JSON.parse(result.value);
207
+ return reply.send(payload);
208
+ }
158
209
  ```
159
210
 
160
- The bundle is treated as **immutable and read-only**.
211
+ > **Note:** The SDK does not maintain rate-limit counters. It signals the throttle parameters; your infrastructure (Redis, middleware, etc.) is responsible for enforcement.
161
212
 
162
213
  ---
163
214
 
164
- ## Polling & Burst Mode
215
+ ## Custom Effects
165
216
 
166
- ```ts
167
- // Force immediate refresh
168
- await client.refreshNow();
217
+ A `custom` effect carries an arbitrary string — including a JSON-encoded object — back to the caller. Common uses: feature flags, A/B variants, per-tenant configuration overlays.
169
218
 
170
- // Temporarily increase polling frequency (e.g. incident response)
171
- await client.refreshNow({ burst: true });
219
+ Enable automatic JSON parsing with `engine.parseCustomEffect`:
220
+
221
+ ```typescript
222
+ const client = new RuntimeClient({
223
+ baseUrl: "https://123456.runtime.govplane.com/",
224
+ runtimeKey: process.env.GP_RUNTIME_KEY!,
225
+ engine: {
226
+ parseCustomEffect: true, // JSON.parse() applied automatically
227
+ },
228
+ });
229
+
230
+ const result = client.evaluate({ target, context });
231
+
232
+ if (result.decision === "custom") {
233
+ const flags = result.parsedValue as { enabled: boolean; variant: string };
234
+ if (flags?.enabled) renderNewCheckout();
235
+ }
172
236
  ```
173
237
 
174
- Polling strategy:
175
- 1. `HEAD /v1/runtime/bundle`
176
- 2. Compare `ETag`
177
- 3. If changed → `GET`
178
- 4. If unchanged → no download, no JSON parse
238
+ Non-JSON strings leave `parsedValue` as `undefined`; the raw `value` string is always present as a fallback.
179
239
 
180
240
  ---
181
241
 
182
- ## Backoff & Degraded Mode
242
+ ## Conditional Rules (`when` / `thenEffect` / `elseEffect`)
183
243
 
184
- On repeated failures, the client:
185
- - Applies exponential backoff with jitter
186
- - Enters **degraded** state after N failures
187
- - Emits status updates
244
+ Rules support an optional `when` condition AST evaluated against the call-time context. The AST is compiled by the control plane and distributed in the bundle — the SDK never executes dynamic code.
188
245
 
189
- ```ts
190
- client.onStatus((status) => {
191
- if (status.state === "degraded") {
192
- console.warn("Runtime degraded:", status);
193
- }
194
- });
246
+ ```
247
+ when absent → always apply effect
248
+ when == true → apply thenEffect (fallback: effect)
249
+ when == false → apply elseEffect (fallback: skip rule)
195
250
  ```
196
251
 
197
- In degraded mode, cached bundles remain active.
252
+ Supported operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `exists`, `and`, `or`, `not`.
253
+
254
+ The `ctx.` prefix on paths is optional and stripped automatically (`ctx.plan` ≡ `plan`).
198
255
 
199
256
  ---
200
257
 
201
- ## Runtime Incident Controls
258
+ ## Policy Defaults
202
259
 
203
- During an incident (DDoS, fraud spike, abuse, misconfiguration), it may be necessary to:
204
- - Force faster policy propagation
205
- - Trigger immediate policy refresh
206
- - Temporarily increase polling frequency (“burst mode”)
260
+ Every policy can declare a `defaults` object the fallback effect when no rule matches that target. Defaults are treated as a synthetic rule with `priority = -1`, so any explicit rule always wins.
207
261
 
208
- Govplane Runtime SDK provides **passive incident controls** that allow operators to react to incidents **without exposing endpoints**, **without handling PII**, and **without recompiling application code**.
262
+ ```json
263
+ {
264
+ "policyKey": "strict-rbac",
265
+ "defaults": { "effect": "deny" },
266
+ "rules": [...]
267
+ }
268
+ ```
209
269
 
210
- These controls are designed to be:
211
- - Local-only
212
- - Zero-network-surface
213
- - Safe under active attack
214
- - Compatible with containerized and orchestrated environments
270
+ All five effect types are supported as defaults: `allow`, `deny`, `throttle`, `kill_switch`, `custom`.
271
+
272
+ ---
215
273
 
216
- ### Supported Incident Control Mechanisms
274
+ ## Runtime Bundle Model
217
275
 
218
- | Mechanism | Restart Required | Network Surface | Hot | Recommended |
219
- |---------|------------------|-----------------|-----|-------------|
220
- | Environment Variable | Usually yes | None | ❌ | ✅ |
221
- | File-based Hot Reload | No | None | ✅ | ⭐ **Primary** |
222
- | POSIX Signal (SIGUSR1) | No | None | ✅ | Optional |
276
+ The SDK retrieves **compiled runtime bundles** generated by the Govplane control plane.
277
+
278
+ ```typescript
279
+ type RuntimeBundleV1 = {
280
+ schemaVersion: 1;
281
+ orgId: string;
282
+ projectId: string;
283
+ env: string;
284
+ generatedAt: string; // ISO timestamp
285
+ bundleVersion?: number;
286
+ checksum?: string; // e.g. "sha256:..."
287
+ policies: RuntimePolicy[];
288
+ };
289
+ ```
290
+
291
+ The bundle is treated as **immutable and read-only**. See [Types & Interfaces](docs/reference/TypesAndInterfaces.md) for the full type tree.
223
292
 
224
293
  ---
225
294
 
226
- **Example: Environment Variable Incident Mode**
295
+ ## Bundle Lifecycle & Polling
227
296
 
228
- If the following environment variable is set, the Runtime SDK automatically enters **incident mode**:
297
+ All `evaluate()` calls read from an in-memory cache there is no network hop at decision time.
229
298
 
230
- ```bash
231
- GP_RUNTIME_INCIDENT=1
299
+ ```typescript
300
+ // Block until first bundle is ready, then start background polling
301
+ await client.warmStart({ timeoutMs: 10_000 });
302
+ client.start();
232
303
  ```
233
304
 
234
- When detected:
235
- • Burst polling is enabled
236
- • Refresh cadence increases
237
- • Degraded recovery is accelerated
305
+ Polling strategy per cycle:
306
+ 1. `HEAD` the bundle endpoint to check the current `ETag`
307
+ 2. If `ETag` unchanged → update metadata, skip the `GET`
308
+ 3. If `ETag` changed → `GET` the full bundle, parse, update cache
309
+ 4. Notify `onUpdate` listeners
310
+
311
+ | Phase | Interval |
312
+ |---|---|
313
+ | Normal | `pollMs` (default **5 000 ms**) |
314
+ | Burst mode | `burstPollMs` (default **500 ms**) for `burstDurationMs` (default **30 000 ms**) |
315
+ | Backoff | Exponential, capped at `backoffMaxMs` |
316
+
317
+ ### Bundle update subscription
318
+
319
+ ```typescript
320
+ client.onUpdate((result) => {
321
+ logger.info("Bundle updated", {
322
+ etag: result.meta.etag,
323
+ bundleVersion: result.meta.bundleVersion,
324
+ updatedAt: result.meta.updatedAt,
325
+ });
326
+ });
327
+ ```
328
+
329
+ ---
238
330
 
239
- Notes
240
- • In most container platforms (ECS, Kubernetes), environment variables cannot be changed at runtime
241
- • A rolling restart is usually required
242
- • For zero-restart response, use File-based Hot Reload
331
+ ## Backoff & Degraded Mode
243
332
 
333
+ On repeated failures the client applies exponential backoff with jitter and enters **degraded mode** after a configurable number of consecutive failures. The cached bundle remains active in degraded mode.
244
334
 
245
- **More details: [Govplane Runtime SDK – Runtime Incident Controls](docs/operations/Govplane_Runtime_Incident_Controls.md)**
335
+ ```typescript
336
+ const client = new RuntimeClient({
337
+ ...
338
+ backoffBaseMs: 500,
339
+ backoffMaxMs: 30_000,
340
+ backoffJitter: 0.2,
341
+ degradeAfterFailures: 3,
342
+ });
343
+
344
+ client.onStatus((status) => {
345
+ if (status.state === "degraded") {
346
+ alerting.trigger("govplane_sdk_degraded", {
347
+ failures: status.consecutiveFailures,
348
+ lastError: status.lastError.message,
349
+ nextRetryAt: status.nextRetryAt,
350
+ });
351
+ }
352
+ });
353
+ ```
354
+
355
+ Backoff formula: `delay = clamp(base × 2^(failures-1), 0, backoffMaxMs) ± jitter%`
246
356
 
247
357
  ---
248
358
 
249
- ## Throttle Decisions
359
+ ## Context Policy & PII Safety
250
360
 
251
- ```ts
252
- if (result.decision === "throttle") {
253
- rateLimiter.apply(
254
- result.throttle.key,
255
- result.throttle.limit,
256
- result.throttle.windowSeconds
257
- );
258
- }
361
+ The `context` object is validated synchronously before every evaluation. A violation throws immediately, before any rule is tested.
362
+
363
+ ```typescript
364
+ const client = new RuntimeClient({
365
+ ...
366
+ engine: {
367
+ contextPolicy: {
368
+ allowedKeys: ["plan", "role", "country", "isAuthenticated", "requestTier"],
369
+ maxStringLen: 64,
370
+ maxArrayLen: 10,
371
+ blockLikelyPiiKeys: true, // blocks email, phone, name, ip, ssn, etc.
372
+ },
373
+ },
374
+ });
259
375
  ```
260
376
 
261
- When multiple throttle rules match, the **most restrictive** one is selected.
377
+ Default allowed keys: `plan`, `country`, `requestTier`, `feature`, `amount`, `isAuthenticated`, `role`.
378
+
379
+ `object` values are not permitted at the top level. Permitted value types: `string`, `number`, `boolean`, `null/undefined`, `string[]`.
380
+
381
+ > Disable validation (`engine: { validateContext: false }`) only in controlled test environments.
262
382
 
263
383
  ---
264
384
 
265
- ## Decision Trace / Explain
385
+ ## Decision Tracing
266
386
 
267
- ### Production (Sampled)
387
+ Use `evaluateWithTrace()` to attach a `DecisionTrace` to the result for observability and debugging. Traces contain only structural metadata — no context values or PII.
268
388
 
269
- ```ts
270
- const out = engine.evaluateWithTrace(
271
- { target, context },
272
- { sampling: 0.01, mode: "compact" }
273
- );
389
+ ### Production (sampled)
274
390
 
275
- if (out.trace) {
276
- console.log(out.trace.summary);
391
+ ```typescript
392
+ const client = new RuntimeClient({
393
+ ...
394
+ trace: {
395
+ defaults: {
396
+ level: "sampled",
397
+ sampling: 0.05, // 5% of evaluations
398
+ budget: { maxTraces: 60, windowMs: 60_000 },
399
+ },
400
+ onDecisionTraceAsync: async (evt) => {
401
+ await analyticsClient.ingest("govplane.trace", evt);
402
+ },
403
+ queueMax: 1000,
404
+ dropPolicy: "drop_new",
405
+ },
406
+ });
407
+
408
+ const result = client.evaluateWithTrace({ target, context });
409
+ if (result.trace) {
410
+ console.log(result.trace.summary);
411
+ console.log(result.trace.winner);
277
412
  }
278
413
  ```
279
414
 
280
- ### Debug (Forced)
415
+ ### Debug (forced)
281
416
 
282
- ```ts
283
- engine.evaluateWithTrace(
417
+ ```typescript
418
+ const result = client.evaluateWithTrace(
284
419
  { target, context },
285
- { force: true, mode: "full" }
420
+ { level: "full", force: true }, // bypass sampling and budget
286
421
  );
287
422
  ```
288
423
 
289
- ### Trace Guarantees
424
+ | Level | Behaviour |
425
+ |---|---|
426
+ | `"off"` | No trace computed. Equivalent to `evaluate()`. |
427
+ | `"errors"` | Trace attached only on `deny` or `kill_switch`. Compact format. |
428
+ | `"sampled"` | Trace attached probabilistically by `sampling` rate and budget. Compact format. |
429
+ | `"full"` | Always attempted (budget permitting). Includes complete per-rule list. |
430
+
431
+ ### Trace sinks
432
+
433
+ Both synchronous (`onDecisionTrace`) and async (`onDecisionTraceAsync`) sinks are supported. Async evaluations are buffered internally and never block evaluation. Drain on shutdown with `client.flushTraces()`.
434
+
435
+ ### Trace guarantees
290
436
 
291
437
  - No rule bodies
292
438
  - No context values
@@ -295,68 +441,106 @@ engine.evaluateWithTrace(
295
441
 
296
442
  ---
297
443
 
298
- ## Async Trace Sink
299
-
300
- ```ts
301
- engine.setTraceSink(
302
- createAsyncTraceSink({
303
- maxQueue: 100,
304
- drop: "drop_new",
305
- sink: async (event) => {
306
- await fetch("https://trace-endpoint", {
307
- method: "POST",
308
- body: JSON.stringify(event)
309
- });
310
- }
311
- })
312
- );
313
- ```
444
+ ## Runtime Incident Controls
314
445
 
315
- The sink:
316
- - Never blocks evaluation
317
- - Never throws
318
- - Drops safely under pressure
446
+ Govplane provides **passive incident controls** that allow operators to respond to incidents without exposing endpoints, handling PII, or recompiling application code.
319
447
 
320
- ---
448
+ | Mechanism | Restart Required | Hot | Recommended |
449
+ |---|---|---|---|
450
+ | Environment Variable (`GP_RUNTIME_INCIDENT=1`) | Usually yes | No | Yes |
451
+ | File-based hot reload (`incidentFilePath`) | No | Yes | **Primary** |
452
+ | POSIX Signal (`SIGUSR1`) | No | Yes | Optional |
321
453
 
322
- ## What This SDK Does NOT Do
454
+ ```typescript
455
+ const client = new RuntimeClient({
456
+ ...
457
+ incidentFilePath: "/etc/govplane/incident.json",
458
+ incidentFilePollMs: 1000,
459
+ incidentSignal: "SIGUSR1",
460
+ });
461
+ ```
323
462
 
324
- - No HTTP middleware
325
- - ❌ No inbound endpoints
326
- - ❌ No request interception
327
- - ❌ No PII handling
328
- - ❌ No policy authoring
329
- - ❌ No dynamic code execution
463
+ Incident file format:
464
+ ```json
465
+ {
466
+ "burst": true,
467
+ "burstDurationMs": 60000,
468
+ "burstPollMs": 200,
469
+ "refreshNow": true
470
+ }
471
+ ```
472
+
473
+ See [Incident Playbook](docs/operations/Govplane_Incident_Playbook.md) and [Incident Controls Reference](docs/operations/Govplane_Runtime_Incident_Controls.md) for step-by-step procedures.
330
474
 
331
475
  ---
332
476
 
333
- ## Security Notes
477
+ ## Using `createPolicyEngine` Directly
334
478
 
335
- - Runtime keys are **read-only**
336
- - Keys are scoped by org / project / env
337
- - Bundles are immutable
338
- - SDK cannot modify state
339
- - Safe to embed in critical paths
479
+ If you manage the bundle yourself (loaded from a file, injected from config), you can use the engine without `RuntimeClient`:
480
+
481
+ ```typescript
482
+ import { createPolicyEngine } from "@govplane/runtime-sdk";
483
+ import { readFileSync } from "node:fs";
484
+
485
+ const bundle = JSON.parse(readFileSync("bundle.json", "utf8"));
340
486
 
341
- ### Threat Model
487
+ const engine = createPolicyEngine({
488
+ getBundle: () => bundle,
489
+ parseCustomEffect: true,
490
+ contextPolicy: {
491
+ allowedKeys: ["plan", "role"],
492
+ maxStringLen: 64,
493
+ maxArrayLen: 10,
494
+ blockLikelyPiiKeys: true,
495
+ },
496
+ });
342
497
 
343
- Please refer to the [Govplane Runtime SDK Threat Model & Security Guarantees](docs/security/Govplane_Threat_Model.md) for a detailed analysis.
498
+ const result = engine.evaluate({ target, context });
499
+ ```
344
500
 
345
501
  ---
346
502
 
347
- ## Performance Notes
503
+ ## What This SDK Does NOT Do
348
504
 
349
- - Designed for second-level polling
350
- - Suitable for APIs, workers, gateways
351
- - Not intended for browser usage
505
+ - No HTTP middleware
506
+ - No inbound endpoints
507
+ - No request interception
508
+ - No PII handling or storage
509
+ - No policy authoring
510
+ - No dynamic code execution
511
+ - No rate-limit counter maintenance
352
512
 
353
513
  ---
354
514
 
355
- ## Versioning & Compatibility
515
+ ## Security Notes
516
+
517
+ - Runtime keys are **read-only** and scoped by org / project / env
518
+ - Bundles are immutable — the SDK cannot modify control-plane state
519
+ - Context validation and PII heuristic blocking are on by default
520
+ - Safe to embed in critical paths
521
+
522
+ See [Govplane Runtime SDK Threat Model](docs/security/Govplane_Threat_Model.md) for a full security analysis.
523
+
524
+ ---
356
525
 
357
- - Bundles are versioned (`schemaVersion`)
358
- - Current version: `RuntimeBundleV1`
359
- - Future versions will be backward-compatible
526
+ ## Documentation
527
+
528
+ | Topic | Link |
529
+ |---|---|
530
+ | Installation & Quick Start | [docs/installation/GettingStarted.md](docs/installation/GettingStarted.md) |
531
+ | Evaluating Decisions | [docs/usage/Evaluate.md](docs/usage/Evaluate.md) |
532
+ | Effect Types | [docs/usage/Effects.md](docs/usage/Effects.md) |
533
+ | Custom Effects | [docs/usage/CustomEffect.md](docs/usage/CustomEffect.md) |
534
+ | Conditional Rules | [docs/usage/ConditionalRules.md](docs/usage/ConditionalRules.md) |
535
+ | Policy Defaults | [docs/usage/PolicyDefaults.md](docs/usage/PolicyDefaults.md) |
536
+ | Context Policy & PII Safety | [docs/usage/ContextPolicy.md](docs/usage/ContextPolicy.md) |
537
+ | Bundle Lifecycle | [docs/usage/BundleLifecycle.md](docs/usage/BundleLifecycle.md) |
538
+ | Decision Tracing | [docs/usage/DecisionTrace.md](docs/usage/DecisionTrace.md) |
539
+ | Configuration Reference | [docs/reference/Configuration.md](docs/reference/Configuration.md) |
540
+ | Types & Interfaces | [docs/reference/TypesAndInterfaces.md](docs/reference/TypesAndInterfaces.md) |
541
+ | Incident Playbook | [docs/operations/Govplane_Incident_Playbook.md](docs/operations/Govplane_Incident_Playbook.md) |
542
+ | Incident Controls Reference | [docs/operations/Govplane_Runtime_Incident_Controls.md](docs/operations/Govplane_Runtime_Incident_Controls.md) |
543
+ | Threat Model | [docs/security/Govplane_Threat_Model.md](docs/security/Govplane_Threat_Model.md) |
360
544
 
361
545
  ---
362
546