@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 +145 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +30 -29
- package/package.json +12 -14
- package/dist/index.d.mts +0 -268
- package/dist/index.mjs +0 -334
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(
|
|
89
|
+
onPoll(response.status, elapsed);
|
|
79
90
|
}
|
|
80
|
-
if (
|
|
91
|
+
if (response.status !== "pending") {
|
|
81
92
|
return {
|
|
82
|
-
approved:
|
|
83
|
-
status:
|
|
84
|
-
comment: response.comment
|
|
85
|
-
resolvedBy: response.resolvedBy
|
|
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
|
-
|
|
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
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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
|
|
21
|
-
"dev": "tsup src/index.ts --format cjs
|
|
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
|
-
"
|
|
24
|
+
"sdk",
|
|
30
25
|
"governance",
|
|
26
|
+
"runtime",
|
|
31
27
|
"security",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
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/
|
|
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
|
-
};
|