@runplane/runplane-sdk 1.0.3 → 1.1.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 ADDED
@@ -0,0 +1,145 @@
1
+ # @runplane/runplane-sdk
2
+
3
+ Official SDK for the Runplane control plane — runtime governance for AI agent actions.
4
+
5
+ Runplane sits between your AI agents and execution. Every action passes through `guard()`, which enforces a decision before your code runs.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @runplane/runplane-sdk
11
+ ```
12
+
13
+ ## Quick Start (CommonJS)
14
+
15
+ ```javascript
16
+ require("dotenv").config()
17
+ const { Runplane } = require("@runplain/runplane-sdk");
18
+ const runplane = new Runplane({
19
+ apiKey: process.env.RUNPLANE_API_KEY,
20
+ })
21
+
22
+ // guard() intercepts execution and enforces the decision
23
+ await runplane.guard(
24
+ "transfer_funds",
25
+ "finance-system",
26
+ { fromAccountId: "acc_1", toAccountId: "acc_2", amount: 400 },
27
+ async () => {
28
+ return await executeTransfer()
29
+ }
30
+ )
31
+ ```
32
+
33
+ ## How It Works
34
+
35
+ `guard()` calls the Runplane API over the network before executing your handler:
36
+
37
+ 1. Sends action + target + context to Runplane
38
+ 2. Runplane evaluates policies and risk
39
+ 3. Returns one of three decisions:
40
+ - **ALLOW** → handler executes immediately
41
+ - **BLOCK** → throws `RunplaneError`, handler never runs
42
+ - **REQUIRE_APPROVAL** → throws `RunplaneError`, awaits human decision
43
+
44
+ ## API Reference
45
+
46
+ ### `new Runplane(config)`
47
+
48
+ Create a new Runplane client.
49
+
50
+ ```javascript
51
+ const runplane = new Runplane({
52
+ apiKey: "your_api_key", // Required
53
+ baseUrl: "https://runplane.ai", // Optional, defaults to https://runplane.ai
54
+ })
55
+ ```
56
+
57
+ ### `guard(action, target, context, handler)`
58
+
59
+ The primary integration method. Wraps your action with Runplane enforcement.
60
+
61
+ ```javascript
62
+ const result = await runplane.guard(
63
+ "delete_record", // action type
64
+ "hr_system", // target resource
65
+ { employeeId: "emp_123" }, // context
66
+ async () => {
67
+ // Your code here - only runs if ALLOW
68
+ return await deleteEmployee("emp_123")
69
+ }
70
+ )
71
+ ```
72
+
73
+ **Returns:** The result of `handler()` if decision is ALLOW.
74
+
75
+ **Throws:** `RunplaneError` if decision is BLOCK or REQUIRE_APPROVAL.
76
+
77
+ ### `decide(payload)`
78
+
79
+ Low-level method to request a decision without automatic enforcement.
80
+
81
+ ```javascript
82
+ const result = await runplane.decide({
83
+ action: "send_email",
84
+ target: "marketing_list",
85
+ context: { recipients: 1200 },
86
+ })
87
+
88
+ console.log(result.decision) // "ALLOW" | "BLOCK" | "REQUIRE_APPROVAL"
89
+ ```
90
+
91
+ ## Handling Decisions
92
+
93
+ ```javascript
94
+ const { Runplane, RunplaneError } = require("@runplane/runplane-sdk")
95
+
96
+ try {
97
+ await runplane.guard("action", "target", {}, async () => {
98
+ // ...
99
+ })
100
+ } catch (err) {
101
+ if (err instanceof RunplaneError) {
102
+ if (err.code === "BLOCK") {
103
+ console.error("Action blocked by policy")
104
+ } else if (err.code === "REQUIRE_APPROVAL") {
105
+ console.log("Waiting for human approval...")
106
+ console.log("Request ID:", err.runplane.requestId)
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Decision Types
113
+
114
+ | Decision | Behavior |
115
+ |----------|----------|
116
+ | `ALLOW` | Handler executes immediately, result returned |
117
+ | `BLOCK` | Throws `RunplaneError` with code `"BLOCK"` |
118
+ | `REQUIRE_APPROVAL` | Throws `RunplaneError` with code `"REQUIRE_APPROVAL"` |
119
+
120
+ ## Error Object
121
+
122
+ When `guard()` throws, the error includes:
123
+
124
+ ```javascript
125
+ {
126
+ message: "Runplane blocked this action",
127
+ code: "BLOCK", // "BLOCK" | "REQUIRE_APPROVAL" | "API_ERROR"
128
+ runplane: { // Full decision response
129
+ decision: "BLOCK",
130
+ reason: "Policy: destructive action on production database",
131
+ requestId: "req_abc123",
132
+ riskScore: 91,
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Requirements
138
+
139
+ - Node.js 18+
140
+ - Valid Runplane API key
141
+ - Network access to `https://runplane.ai`
142
+
143
+ ## License
144
+
145
+ MIT
package/dist/index.d.ts CHANGED
@@ -259,9 +259,21 @@ declare class RunplaneError extends Error {
259
259
  /** Request ID associated with this error, if available */
260
260
  readonly requestId?: string;
261
261
  constructor(message: string, code: RunplaneErrorCode, requestId?: string);
262
+ /**
263
+ * Check if this error indicates the action was blocked
264
+ */
262
265
  isBlocked(): boolean;
266
+ /**
267
+ * Check if this error indicates the action was denied by an approver
268
+ */
263
269
  isDenied(): boolean;
270
+ /**
271
+ * Check if this error indicates a timeout occurred
272
+ */
264
273
  isTimeout(): boolean;
274
+ /**
275
+ * Check if this error is due to network issues
276
+ */
265
277
  isNetworkError(): boolean;
266
278
  }
267
279
 
package/dist/index.js CHANGED
@@ -36,15 +36,27 @@ var RunplaneError = class _RunplaneError extends Error {
36
36
  Error.captureStackTrace(this, _RunplaneError);
37
37
  }
38
38
  }
39
+ /**
40
+ * Check if this error indicates the action was blocked
41
+ */
39
42
  isBlocked() {
40
43
  return this.code === "BLOCKED";
41
44
  }
45
+ /**
46
+ * Check if this error indicates the action was denied by an approver
47
+ */
42
48
  isDenied() {
43
49
  return this.code === "DENIED";
44
50
  }
51
+ /**
52
+ * Check if this error indicates a timeout occurred
53
+ */
45
54
  isTimeout() {
46
55
  return this.code === "TIMEOUT";
47
56
  }
57
+ /**
58
+ * Check if this error is due to network issues
59
+ */
48
60
  isNetworkError() {
49
61
  return this.code === "NETWORK_ERROR";
50
62
  }
@@ -73,16 +85,15 @@ var ApprovalPoller = class {
73
85
  try {
74
86
  const response = await this.fetchApprovalStatus(requestId);
75
87
  const elapsed = Date.now() - startTime;
76
- const normalizedStatus = this.normalizeStatus(response);
77
88
  if (onPoll) {
78
- onPoll(normalizedStatus, elapsed);
89
+ onPoll(response.status, elapsed);
79
90
  }
80
- if (normalizedStatus !== "pending") {
91
+ if (response.status !== "pending") {
81
92
  return {
82
- approved: normalizedStatus === "approved",
83
- status: normalizedStatus,
84
- comment: response.comment || response.reason,
85
- resolvedBy: response.resolvedBy || response.approvedBy
93
+ approved: response.status === "approved",
94
+ status: response.status,
95
+ comment: response.comment,
96
+ resolvedBy: response.resolvedBy
86
97
  };
87
98
  }
88
99
  await this.sleep(interval);
@@ -105,7 +116,7 @@ var ApprovalPoller = class {
105
116
  `${this.baseUrl}/api/approvals/poll/${requestId}`,
106
117
  {
107
118
  headers: {
108
- Authorization: `Bearer ${this.apiKey}`
119
+ "Authorization": `Bearer ${this.apiKey}`
109
120
  }
110
121
  }
111
122
  );
@@ -114,26 +125,6 @@ var ApprovalPoller = class {
114
125
  }
115
126
  return response.json();
116
127
  }
117
- /**
118
- * Normalize server response → SDK canonical format
119
- */
120
- normalizeStatus(response) {
121
- const raw = response.status || response.decision || response.decisionOutcome || "";
122
- const normalized = String(raw).toLowerCase();
123
- if (normalized === "approved" || normalized === "allow") {
124
- return "approved";
125
- }
126
- if (normalized === "denied" || normalized === "deny" || normalized === "block") {
127
- return "denied";
128
- }
129
- if (normalized === "pending") {
130
- return "pending";
131
- }
132
- if (normalized === "expired") {
133
- return "expired";
134
- }
135
- return "pending";
136
- }
137
128
  sleep(ms) {
138
129
  return new Promise((resolve) => setTimeout(resolve, ms));
139
130
  }
@@ -233,8 +224,18 @@ var Runplane = class {
233
224
  severity: json.severity
234
225
  };
235
226
  } catch (error) {
227
+ const isAbort = error instanceof Error && error.name === "AbortError";
236
228
  const message = error instanceof Error ? error.message : "Unknown error";
237
- return this.handleFailure(`Network failure: ${message}`, request);
229
+ const diagnostics = {
230
+ baseUrl: this.baseUrl,
231
+ timeoutMs: this.timeoutMs,
232
+ failMode: this.failMode,
233
+ errorType: isAbort ? "timeout" : "network",
234
+ errorMessage: message
235
+ };
236
+ const failureReason = isAbort ? `Request timeout after ${this.timeoutMs}ms (baseUrl: ${this.baseUrl})` : `Network failure: ${message}`;
237
+ console.error("[runplane-sdk] Decision request failed:", diagnostics);
238
+ return this.handleFailure(failureReason, request);
238
239
  } finally {
239
240
  clearTimeout(timeout);
240
241
  }
package/package.json CHANGED
@@ -1,15 +1,11 @@
1
1
  {
2
2
  "name": "@runplane/runplane-sdk",
3
- "version": "1.0.3",
4
- "description": "Official SDK for the Runplane control plane - runtime governance for AI agent actions",
3
+ "version": "1.1.0",
4
+ "description": "Runtime governance SDK for AI agent actions. Wrap sensitive operations with guard() to enforce ALLOW, BLOCK, or REQUIRE_APPROVAL decisions before execution.",
5
5
  "main": "./dist/index.js",
6
- "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
8
6
  "exports": {
9
7
  ".": {
10
- "import": "./dist/index.mjs",
11
- "require": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
8
+ "require": "./dist/index.js"
13
9
  }
14
10
  },
15
11
  "files": [
@@ -17,20 +13,22 @@
17
13
  "README.md"
18
14
  ],
19
15
  "scripts": {
20
- "build": "tsup src/index.ts --format cjs,esm --dts",
21
- "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
16
+ "build": "tsup src/index.ts --format cjs --dts",
17
+ "dev": "tsup src/index.ts --format cjs --dts --watch",
22
18
  "lint": "eslint src/",
23
19
  "test": "vitest",
24
20
  "prepublishOnly": "npm run build"
25
21
  },
26
22
  "keywords": [
27
- "runplane",
28
23
  "ai",
29
- "agents",
24
+ "sdk",
30
25
  "governance",
26
+ "runtime",
31
27
  "security",
32
- "containment",
33
- "policy",
28
+ "agent",
29
+ "control-plane",
30
+ "guard",
31
+ "runplane",
34
32
  "llm",
35
33
  "langchain",
36
34
  "openai"
@@ -41,7 +39,7 @@
41
39
  "type": "git",
42
40
  "url": "https://github.com/runplane/sdk.git"
43
41
  },
44
- "homepage": "https://runplane.ai/docs",
42
+ "homepage": "https://runplane.ai/developer",
45
43
  "bugs": {
46
44
  "url": "https://github.com/runplane/sdk/issues"
47
45
  },
package/dist/index.d.mts DELETED
@@ -1,268 +0,0 @@
1
- /**
2
- * @runplane/runplane-sdk - TypeScript types for the Runplane SDK
3
- */
4
- /** Decision returned by the Runplane control plane */
5
- type Decision = "ALLOW" | "BLOCK" | "REQUIRE_APPROVAL";
6
- /** Approval status for pending actions */
7
- type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
8
- /** Behavior when the API is unreachable or times out */
9
- type FailMode = "open" | "closed";
10
- /**
11
- * Configuration options for the Runplane client
12
- */
13
- interface RunplaneConfig {
14
- /** Your Runplane API key (starts with ars_) */
15
- apiKey: string;
16
- /** Base URL for the Runplane API. Defaults to https://runplane.ai */
17
- baseUrl?: string;
18
- /** Timeout for API requests in milliseconds. Defaults to 3000 */
19
- timeoutMs?: number;
20
- /**
21
- * Behavior when API is unreachable:
22
- * - "closed" (default): Block actions when API is unavailable
23
- * - "open": Allow actions when API is unavailable (less secure)
24
- */
25
- failMode?: FailMode;
26
- /** Timeout for approval polling in milliseconds. Defaults to 300000 (5 min) */
27
- approvalTimeoutMs?: number;
28
- /** Interval between approval polls in milliseconds. Defaults to 2000 */
29
- approvalPollIntervalMs?: number;
30
- }
31
- /**
32
- * Request payload for the decide endpoint
33
- */
34
- interface DecideRequest {
35
- /** Type of action being performed (e.g., "send_email", "delete_record") */
36
- actionType: string;
37
- /** Target resource or destination (e.g., "prod-db", "user@example.com") */
38
- target: string;
39
- /** Additional context for risk evaluation */
40
- context?: Record<string, unknown>;
41
- /** Optional request ID for idempotency */
42
- requestId?: string;
43
- }
44
- /**
45
- * Response from the decide endpoint
46
- */
47
- interface DecideResponse {
48
- /** The decision: ALLOW, BLOCK, or REQUIRE_APPROVAL */
49
- decision: Decision;
50
- /** Human-readable reason for the decision */
51
- reason: string;
52
- /** Unique identifier for this request (use for approval polling) */
53
- requestId: string;
54
- /** The policy rule that matched, if any */
55
- matchedRule?: string;
56
- /** Computed risk score (0-100) */
57
- riskScore?: number;
58
- /** Risk severity classification */
59
- severity?: string;
60
- }
61
- /**
62
- * Response from the approval polling endpoint
63
- */
64
- interface ApprovalPollResponse {
65
- /** Current status of the approval request */
66
- status: ApprovalStatus;
67
- /** ISO timestamp when the approval was resolved */
68
- resolvedAt?: string;
69
- /** User who approved/denied the request */
70
- resolvedBy?: string;
71
- /** Optional comment from the approver */
72
- comment?: string;
73
- }
74
- /**
75
- * Options for waitForApproval method
76
- */
77
- interface WaitForApprovalOptions {
78
- /** Override the default approval timeout */
79
- timeoutMs?: number;
80
- /** Override the default poll interval */
81
- pollIntervalMs?: number;
82
- /** Callback invoked on each poll (for progress updates) */
83
- onPoll?: (status: ApprovalStatus, elapsedMs: number) => void;
84
- }
85
- /**
86
- * Result from waitForApproval method
87
- */
88
- interface WaitForApprovalResult {
89
- /** Whether the action was approved */
90
- approved: boolean;
91
- /** Final status */
92
- status: ApprovalStatus;
93
- /** Optional comment from the approver */
94
- comment?: string;
95
- /** User who resolved the request */
96
- resolvedBy?: string;
97
- }
98
- /**
99
- * Error codes for RunplaneError
100
- */
101
- type RunplaneErrorCode = "BLOCKED" | "DENIED" | "TIMEOUT" | "NETWORK_ERROR" | "INVALID_CONFIG" | "UNKNOWN";
102
-
103
- /**
104
- * @runplane/runplane-sdk - Main client for the Runplane API
105
- */
106
-
107
- /**
108
- * Runplane SDK Client
109
- *
110
- * The main entry point for interacting with the Runplane control plane.
111
- * Use this to request execution clearance before performing sensitive actions.
112
- *
113
- * @example
114
- * ```typescript
115
- * import { Runplane } from "@runplane/runplane-sdk";
116
- *
117
- * const runplane = new Runplane({
118
- * apiKey: process.env.RUNPLANE_SYSTEM_KEY!,
119
- * baseUrl: "https://runplane.ai",
120
- * failMode: "closed"
121
- * });
122
- *
123
- * const decision = await runplane.decide({
124
- * actionType: "send_email",
125
- * target: "marketing_list",
126
- * context: { recipients: 1200 }
127
- * });
128
- *
129
- * if (decision.decision === "ALLOW") {
130
- * // Proceed with the action
131
- * }
132
- * ```
133
- */
134
- declare class Runplane {
135
- private readonly baseUrl;
136
- private readonly apiKey;
137
- private readonly timeoutMs;
138
- private readonly failMode;
139
- private readonly approvalPoller;
140
- constructor(config: RunplaneConfig);
141
- /**
142
- * Request a decision from the Runplane control plane.
143
- *
144
- * Call this before executing any sensitive action. The response will indicate
145
- * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
146
- *
147
- * @param request - The action details to evaluate
148
- * @returns The decision response
149
- *
150
- * @example
151
- * ```typescript
152
- * const decision = await runplane.decide({
153
- * actionType: "delete_record",
154
- * target: "users.prod",
155
- * context: { recordId: "usr_123", reason: "gdpr_request" }
156
- * });
157
- *
158
- * switch (decision.decision) {
159
- * case "ALLOW":
160
- * await deleteRecord(recordId);
161
- * break;
162
- * case "BLOCK":
163
- * console.error("Action blocked:", decision.reason);
164
- * break;
165
- * case "REQUIRE_APPROVAL":
166
- * // Wait for human approval
167
- * const result = await runplane.waitForApproval(decision.requestId);
168
- * if (result.approved) {
169
- * await deleteRecord(recordId);
170
- * }
171
- * break;
172
- * }
173
- * ```
174
- */
175
- decide(request: DecideRequest): Promise<DecideResponse>;
176
- /**
177
- * Wait for an approval decision on a pending request.
178
- *
179
- * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
180
- * the approval endpoint until the request is approved, denied, or times out.
181
- *
182
- * @param requestId - The requestId from the decide() response
183
- * @param options - Optional configuration for polling behavior
184
- * @returns The approval result
185
- *
186
- * @example
187
- * ```typescript
188
- * const decision = await runplane.decide({ ... });
189
- *
190
- * if (decision.decision === "REQUIRE_APPROVAL") {
191
- * console.log("Waiting for approval...");
192
- *
193
- * const result = await runplane.waitForApproval(decision.requestId, {
194
- * timeoutMs: 600000, // 10 minutes
195
- * onPoll: (status, elapsed) => {
196
- * console.log(`Still waiting... ${elapsed}ms elapsed`);
197
- * }
198
- * });
199
- *
200
- * if (result.approved) {
201
- * console.log("Approved by:", result.resolvedBy);
202
- * } else {
203
- * console.log("Denied:", result.comment);
204
- * }
205
- * }
206
- * ```
207
- */
208
- waitForApproval(requestId: string, options?: WaitForApprovalOptions): Promise<WaitForApprovalResult>;
209
- /**
210
- * Guard a function with containment evaluation.
211
- *
212
- * This is a convenience method that wraps decide() and waitForApproval()
213
- * into a single call. If the action is blocked or denied, it throws.
214
- * If approved, it executes the provided function.
215
- *
216
- * @param actionType - Type of action being performed
217
- * @param target - Target resource
218
- * @param context - Additional context
219
- * @param fn - Function to execute if allowed
220
- * @returns The result of fn()
221
- * @throws RunplaneError if blocked, denied, or times out (in closed mode)
222
- *
223
- * @example
224
- * ```typescript
225
- * const result = await runplane.guard(
226
- * "payment_transfer",
227
- * "external_bank",
228
- * { amount: 50000, currency: "USD" },
229
- * async () => {
230
- * return await paymentService.transfer(amount, recipient);
231
- * }
232
- * );
233
- * ```
234
- */
235
- guard<T>(actionType: string, target: string, context: Record<string, unknown> | null, fn: () => Promise<T>): Promise<T>;
236
- /**
237
- * Handle API failures according to failMode
238
- */
239
- private handleFailure;
240
- }
241
-
242
- /**
243
- * @runplane/runplane-sdk - Error handling
244
- */
245
-
246
- /**
247
- * Error thrown by the Runplane SDK
248
- *
249
- * This error is thrown when:
250
- * - An action is blocked by policy
251
- * - An approval request is denied
252
- * - Approval times out (in fail-closed mode)
253
- * - Network errors occur (in fail-closed mode)
254
- * - Invalid configuration is provided
255
- */
256
- declare class RunplaneError extends Error {
257
- /** Error classification code */
258
- readonly code: RunplaneErrorCode;
259
- /** Request ID associated with this error, if available */
260
- readonly requestId?: string;
261
- constructor(message: string, code: RunplaneErrorCode, requestId?: string);
262
- isBlocked(): boolean;
263
- isDenied(): boolean;
264
- isTimeout(): boolean;
265
- isNetworkError(): boolean;
266
- }
267
-
268
- export { type ApprovalPollResponse, type ApprovalStatus, type DecideRequest, type DecideResponse, type Decision, type FailMode, Runplane, type RunplaneConfig, RunplaneError, type RunplaneErrorCode, type WaitForApprovalOptions, type WaitForApprovalResult };
package/dist/index.mjs DELETED
@@ -1,334 +0,0 @@
1
- // src/error.ts
2
- var RunplaneError = class _RunplaneError extends Error {
3
- constructor(message, code, requestId) {
4
- super(message);
5
- this.name = "RunplaneError";
6
- this.code = code;
7
- this.requestId = requestId;
8
- if (Error.captureStackTrace) {
9
- Error.captureStackTrace(this, _RunplaneError);
10
- }
11
- }
12
- isBlocked() {
13
- return this.code === "BLOCKED";
14
- }
15
- isDenied() {
16
- return this.code === "DENIED";
17
- }
18
- isTimeout() {
19
- return this.code === "TIMEOUT";
20
- }
21
- isNetworkError() {
22
- return this.code === "NETWORK_ERROR";
23
- }
24
- };
25
-
26
- // src/approval.ts
27
- var ApprovalPoller = class {
28
- // Cap at 10 seconds
29
- constructor(config) {
30
- this.maxPollIntervalMs = 1e4;
31
- this.baseUrl = config.baseUrl;
32
- this.apiKey = config.apiKey;
33
- this.defaultTimeoutMs = config.timeoutMs;
34
- this.defaultPollIntervalMs = config.pollIntervalMs;
35
- }
36
- /**
37
- * Poll for approval status until resolved or timeout
38
- */
39
- async poll(requestId, options) {
40
- const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
41
- const initialInterval = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
42
- const onPoll = options?.onPoll;
43
- const startTime = Date.now();
44
- let interval = initialInterval;
45
- while (Date.now() - startTime < timeoutMs) {
46
- try {
47
- const response = await this.fetchApprovalStatus(requestId);
48
- const elapsed = Date.now() - startTime;
49
- const normalizedStatus = this.normalizeStatus(response);
50
- if (onPoll) {
51
- onPoll(normalizedStatus, elapsed);
52
- }
53
- if (normalizedStatus !== "pending") {
54
- return {
55
- approved: normalizedStatus === "approved",
56
- status: normalizedStatus,
57
- comment: response.comment || response.reason,
58
- resolvedBy: response.resolvedBy || response.approvedBy
59
- };
60
- }
61
- await this.sleep(interval);
62
- interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
63
- } catch {
64
- await this.sleep(interval);
65
- interval = Math.min(interval * 2, this.maxPollIntervalMs);
66
- }
67
- }
68
- return {
69
- approved: false,
70
- status: "expired"
71
- };
72
- }
73
- /**
74
- * Fetch current approval status from the API
75
- */
76
- async fetchApprovalStatus(requestId) {
77
- const response = await fetch(
78
- `${this.baseUrl}/api/approvals/poll/${requestId}`,
79
- {
80
- headers: {
81
- Authorization: `Bearer ${this.apiKey}`
82
- }
83
- }
84
- );
85
- if (!response.ok) {
86
- throw new Error(`Failed to fetch approval status: ${response.statusText}`);
87
- }
88
- return response.json();
89
- }
90
- /**
91
- * Normalize server response → SDK canonical format
92
- */
93
- normalizeStatus(response) {
94
- const raw = response.status || response.decision || response.decisionOutcome || "";
95
- const normalized = String(raw).toLowerCase();
96
- if (normalized === "approved" || normalized === "allow") {
97
- return "approved";
98
- }
99
- if (normalized === "denied" || normalized === "deny" || normalized === "block") {
100
- return "denied";
101
- }
102
- if (normalized === "pending") {
103
- return "pending";
104
- }
105
- if (normalized === "expired") {
106
- return "expired";
107
- }
108
- return "pending";
109
- }
110
- sleep(ms) {
111
- return new Promise((resolve) => setTimeout(resolve, ms));
112
- }
113
- };
114
-
115
- // src/client.ts
116
- var DEFAULT_BASE_URL = "https://runplane.ai";
117
- var DEFAULT_TIMEOUT_MS = 3e3;
118
- var DEFAULT_FAIL_MODE = "closed";
119
- var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
120
- var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
121
- var Runplane = class {
122
- constructor(config) {
123
- if (!config.apiKey) {
124
- throw new RunplaneError(
125
- "API key is required",
126
- "INVALID_CONFIG"
127
- );
128
- }
129
- this.apiKey = config.apiKey;
130
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
131
- this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
132
- this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
133
- this.approvalPoller = new ApprovalPoller({
134
- baseUrl: this.baseUrl,
135
- apiKey: this.apiKey,
136
- timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
137
- pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
138
- });
139
- }
140
- /**
141
- * Request a decision from the Runplane control plane.
142
- *
143
- * Call this before executing any sensitive action. The response will indicate
144
- * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
145
- *
146
- * @param request - The action details to evaluate
147
- * @returns The decision response
148
- *
149
- * @example
150
- * ```typescript
151
- * const decision = await runplane.decide({
152
- * actionType: "delete_record",
153
- * target: "users.prod",
154
- * context: { recordId: "usr_123", reason: "gdpr_request" }
155
- * });
156
- *
157
- * switch (decision.decision) {
158
- * case "ALLOW":
159
- * await deleteRecord(recordId);
160
- * break;
161
- * case "BLOCK":
162
- * console.error("Action blocked:", decision.reason);
163
- * break;
164
- * case "REQUIRE_APPROVAL":
165
- * // Wait for human approval
166
- * const result = await runplane.waitForApproval(decision.requestId);
167
- * if (result.approved) {
168
- * await deleteRecord(recordId);
169
- * }
170
- * break;
171
- * }
172
- * ```
173
- */
174
- async decide(request) {
175
- const controller = new AbortController();
176
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
177
- try {
178
- const response = await fetch(`${this.baseUrl}/api/decide`, {
179
- method: "POST",
180
- headers: {
181
- "Content-Type": "application/json",
182
- "Authorization": `Bearer ${this.apiKey}`
183
- },
184
- body: JSON.stringify({
185
- agentKey: this.apiKey,
186
- actionType: request.actionType,
187
- target: request.target,
188
- context: request.context ?? {},
189
- requestId: request.requestId
190
- }),
191
- signal: controller.signal
192
- });
193
- const json = await response.json();
194
- if (!response.ok) {
195
- return this.handleFailure(
196
- `API error: ${json.error ?? response.statusText}`,
197
- request
198
- );
199
- }
200
- return {
201
- decision: json.decision,
202
- reason: json.reason,
203
- requestId: json.requestId,
204
- matchedRule: json.matchedRule,
205
- riskScore: json.riskScore,
206
- severity: json.severity
207
- };
208
- } catch (error) {
209
- const message = error instanceof Error ? error.message : "Unknown error";
210
- return this.handleFailure(`Network failure: ${message}`, request);
211
- } finally {
212
- clearTimeout(timeout);
213
- }
214
- }
215
- /**
216
- * Wait for an approval decision on a pending request.
217
- *
218
- * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
219
- * the approval endpoint until the request is approved, denied, or times out.
220
- *
221
- * @param requestId - The requestId from the decide() response
222
- * @param options - Optional configuration for polling behavior
223
- * @returns The approval result
224
- *
225
- * @example
226
- * ```typescript
227
- * const decision = await runplane.decide({ ... });
228
- *
229
- * if (decision.decision === "REQUIRE_APPROVAL") {
230
- * console.log("Waiting for approval...");
231
- *
232
- * const result = await runplane.waitForApproval(decision.requestId, {
233
- * timeoutMs: 600000, // 10 minutes
234
- * onPoll: (status, elapsed) => {
235
- * console.log(`Still waiting... ${elapsed}ms elapsed`);
236
- * }
237
- * });
238
- *
239
- * if (result.approved) {
240
- * console.log("Approved by:", result.resolvedBy);
241
- * } else {
242
- * console.log("Denied:", result.comment);
243
- * }
244
- * }
245
- * ```
246
- */
247
- async waitForApproval(requestId, options) {
248
- return this.approvalPoller.poll(requestId, options);
249
- }
250
- /**
251
- * Guard a function with containment evaluation.
252
- *
253
- * This is a convenience method that wraps decide() and waitForApproval()
254
- * into a single call. If the action is blocked or denied, it throws.
255
- * If approved, it executes the provided function.
256
- *
257
- * @param actionType - Type of action being performed
258
- * @param target - Target resource
259
- * @param context - Additional context
260
- * @param fn - Function to execute if allowed
261
- * @returns The result of fn()
262
- * @throws RunplaneError if blocked, denied, or times out (in closed mode)
263
- *
264
- * @example
265
- * ```typescript
266
- * const result = await runplane.guard(
267
- * "payment_transfer",
268
- * "external_bank",
269
- * { amount: 50000, currency: "USD" },
270
- * async () => {
271
- * return await paymentService.transfer(amount, recipient);
272
- * }
273
- * );
274
- * ```
275
- */
276
- async guard(actionType, target, context, fn) {
277
- const response = await this.decide({
278
- actionType,
279
- target,
280
- context: context ?? void 0
281
- });
282
- if (response.decision === "BLOCK") {
283
- throw new RunplaneError(
284
- `Action blocked: ${response.reason}`,
285
- "BLOCKED",
286
- response.requestId
287
- );
288
- }
289
- if (response.decision === "ALLOW") {
290
- return fn();
291
- }
292
- const approval = await this.waitForApproval(response.requestId);
293
- if (approval.approved) {
294
- return fn();
295
- }
296
- if (approval.status === "denied") {
297
- throw new RunplaneError(
298
- `Action denied: ${approval.comment || "No reason provided"}`,
299
- "DENIED",
300
- response.requestId
301
- );
302
- }
303
- if (this.failMode === "open") {
304
- return fn();
305
- }
306
- throw new RunplaneError(
307
- "Approval timeout - action blocked (fail-closed)",
308
- "TIMEOUT",
309
- response.requestId
310
- );
311
- }
312
- /**
313
- * Handle API failures according to failMode
314
- */
315
- handleFailure(reason, request) {
316
- const requestId = request.requestId ?? crypto.randomUUID();
317
- if (this.failMode === "open") {
318
- return {
319
- decision: "ALLOW",
320
- reason: `${reason} (fail-open)`,
321
- requestId
322
- };
323
- }
324
- return {
325
- decision: "BLOCK",
326
- reason: `${reason} (fail-closed)`,
327
- requestId
328
- };
329
- }
330
- };
331
- export {
332
- Runplane,
333
- RunplaneError
334
- };