@runplane/runplane-sdk 1.0.1 → 1.0.2
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/dist/index.d.mts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +341 -21
- package/dist/index.mjs +313 -0
- package/package.json +59 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
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.d.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
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.js
CHANGED
|
@@ -1,21 +1,341 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Runplane: () => Runplane,
|
|
24
|
+
RunplaneError: () => RunplaneError
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/error.ts
|
|
29
|
+
var RunplaneError = class _RunplaneError extends Error {
|
|
30
|
+
constructor(message, code, requestId) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "RunplaneError";
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.requestId = requestId;
|
|
35
|
+
if (Error.captureStackTrace) {
|
|
36
|
+
Error.captureStackTrace(this, _RunplaneError);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
isBlocked() {
|
|
40
|
+
return this.code === "BLOCKED";
|
|
41
|
+
}
|
|
42
|
+
isDenied() {
|
|
43
|
+
return this.code === "DENIED";
|
|
44
|
+
}
|
|
45
|
+
isTimeout() {
|
|
46
|
+
return this.code === "TIMEOUT";
|
|
47
|
+
}
|
|
48
|
+
isNetworkError() {
|
|
49
|
+
return this.code === "NETWORK_ERROR";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/approval.ts
|
|
54
|
+
var ApprovalPoller = class {
|
|
55
|
+
// Cap at 10 seconds
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.maxPollIntervalMs = 1e4;
|
|
58
|
+
this.baseUrl = config.baseUrl;
|
|
59
|
+
this.apiKey = config.apiKey;
|
|
60
|
+
this.defaultTimeoutMs = config.timeoutMs;
|
|
61
|
+
this.defaultPollIntervalMs = config.pollIntervalMs;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Poll for approval status until resolved or timeout
|
|
65
|
+
*/
|
|
66
|
+
async poll(requestId, options) {
|
|
67
|
+
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
|
68
|
+
const initialInterval = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
|
|
69
|
+
const onPoll = options?.onPoll;
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
let interval = initialInterval;
|
|
72
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
73
|
+
try {
|
|
74
|
+
const response = await this.fetchApprovalStatus(requestId);
|
|
75
|
+
const elapsed = Date.now() - startTime;
|
|
76
|
+
if (onPoll) {
|
|
77
|
+
onPoll(response.status, elapsed);
|
|
78
|
+
}
|
|
79
|
+
if (response.status !== "pending") {
|
|
80
|
+
return {
|
|
81
|
+
approved: response.status === "approved",
|
|
82
|
+
status: response.status,
|
|
83
|
+
comment: response.comment,
|
|
84
|
+
resolvedBy: response.resolvedBy
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
await this.sleep(interval);
|
|
88
|
+
interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
|
|
89
|
+
} catch {
|
|
90
|
+
await this.sleep(interval);
|
|
91
|
+
interval = Math.min(interval * 2, this.maxPollIntervalMs);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
approved: false,
|
|
96
|
+
status: "expired"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fetch current approval status from the API
|
|
101
|
+
*/
|
|
102
|
+
async fetchApprovalStatus(requestId) {
|
|
103
|
+
const response = await fetch(
|
|
104
|
+
`${this.baseUrl}/api/approvals/poll/${requestId}`,
|
|
105
|
+
{
|
|
106
|
+
headers: {
|
|
107
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`Failed to fetch approval status: ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
return response.json();
|
|
115
|
+
}
|
|
116
|
+
sleep(ms) {
|
|
117
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/client.ts
|
|
122
|
+
var DEFAULT_BASE_URL = "https://runplane.ai";
|
|
123
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
124
|
+
var DEFAULT_FAIL_MODE = "closed";
|
|
125
|
+
var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
126
|
+
var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
|
|
127
|
+
var Runplane = class {
|
|
128
|
+
constructor(config) {
|
|
129
|
+
if (!config.apiKey) {
|
|
130
|
+
throw new RunplaneError(
|
|
131
|
+
"API key is required",
|
|
132
|
+
"INVALID_CONFIG"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
this.apiKey = config.apiKey;
|
|
136
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
137
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
138
|
+
this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
|
|
139
|
+
this.approvalPoller = new ApprovalPoller({
|
|
140
|
+
baseUrl: this.baseUrl,
|
|
141
|
+
apiKey: this.apiKey,
|
|
142
|
+
timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
143
|
+
pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Request a decision from the Runplane control plane.
|
|
148
|
+
*
|
|
149
|
+
* Call this before executing any sensitive action. The response will indicate
|
|
150
|
+
* whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
|
|
151
|
+
*
|
|
152
|
+
* @param request - The action details to evaluate
|
|
153
|
+
* @returns The decision response
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const decision = await runplane.decide({
|
|
158
|
+
* actionType: "delete_record",
|
|
159
|
+
* target: "users.prod",
|
|
160
|
+
* context: { recordId: "usr_123", reason: "gdpr_request" }
|
|
161
|
+
* });
|
|
162
|
+
*
|
|
163
|
+
* switch (decision.decision) {
|
|
164
|
+
* case "ALLOW":
|
|
165
|
+
* await deleteRecord(recordId);
|
|
166
|
+
* break;
|
|
167
|
+
* case "BLOCK":
|
|
168
|
+
* console.error("Action blocked:", decision.reason);
|
|
169
|
+
* break;
|
|
170
|
+
* case "REQUIRE_APPROVAL":
|
|
171
|
+
* // Wait for human approval
|
|
172
|
+
* const result = await runplane.waitForApproval(decision.requestId);
|
|
173
|
+
* if (result.approved) {
|
|
174
|
+
* await deleteRecord(recordId);
|
|
175
|
+
* }
|
|
176
|
+
* break;
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
async decide(request) {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${this.baseUrl}/api/decide`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
agentKey: this.apiKey,
|
|
192
|
+
actionType: request.actionType,
|
|
193
|
+
target: request.target,
|
|
194
|
+
context: request.context ?? {},
|
|
195
|
+
requestId: request.requestId
|
|
196
|
+
}),
|
|
197
|
+
signal: controller.signal
|
|
198
|
+
});
|
|
199
|
+
const json = await response.json();
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
return this.handleFailure(
|
|
202
|
+
`API error: ${json.error ?? response.statusText}`,
|
|
203
|
+
request
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
decision: json.decision,
|
|
208
|
+
reason: json.reason,
|
|
209
|
+
requestId: json.requestId,
|
|
210
|
+
matchedRule: json.matchedRule,
|
|
211
|
+
riskScore: json.riskScore,
|
|
212
|
+
severity: json.severity
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
216
|
+
return this.handleFailure(`Network failure: ${message}`, request);
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timeout);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Wait for an approval decision on a pending request.
|
|
223
|
+
*
|
|
224
|
+
* Use this when decide() returns REQUIRE_APPROVAL. This method will poll
|
|
225
|
+
* the approval endpoint until the request is approved, denied, or times out.
|
|
226
|
+
*
|
|
227
|
+
* @param requestId - The requestId from the decide() response
|
|
228
|
+
* @param options - Optional configuration for polling behavior
|
|
229
|
+
* @returns The approval result
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const decision = await runplane.decide({ ... });
|
|
234
|
+
*
|
|
235
|
+
* if (decision.decision === "REQUIRE_APPROVAL") {
|
|
236
|
+
* console.log("Waiting for approval...");
|
|
237
|
+
*
|
|
238
|
+
* const result = await runplane.waitForApproval(decision.requestId, {
|
|
239
|
+
* timeoutMs: 600000, // 10 minutes
|
|
240
|
+
* onPoll: (status, elapsed) => {
|
|
241
|
+
* console.log(`Still waiting... ${elapsed}ms elapsed`);
|
|
242
|
+
* }
|
|
243
|
+
* });
|
|
244
|
+
*
|
|
245
|
+
* if (result.approved) {
|
|
246
|
+
* console.log("Approved by:", result.resolvedBy);
|
|
247
|
+
* } else {
|
|
248
|
+
* console.log("Denied:", result.comment);
|
|
249
|
+
* }
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
async waitForApproval(requestId, options) {
|
|
254
|
+
return this.approvalPoller.poll(requestId, options);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Guard a function with containment evaluation.
|
|
258
|
+
*
|
|
259
|
+
* This is a convenience method that wraps decide() and waitForApproval()
|
|
260
|
+
* into a single call. If the action is blocked or denied, it throws.
|
|
261
|
+
* If approved, it executes the provided function.
|
|
262
|
+
*
|
|
263
|
+
* @param actionType - Type of action being performed
|
|
264
|
+
* @param target - Target resource
|
|
265
|
+
* @param context - Additional context
|
|
266
|
+
* @param fn - Function to execute if allowed
|
|
267
|
+
* @returns The result of fn()
|
|
268
|
+
* @throws RunplaneError if blocked, denied, or times out (in closed mode)
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* const result = await runplane.guard(
|
|
273
|
+
* "payment_transfer",
|
|
274
|
+
* "external_bank",
|
|
275
|
+
* { amount: 50000, currency: "USD" },
|
|
276
|
+
* async () => {
|
|
277
|
+
* return await paymentService.transfer(amount, recipient);
|
|
278
|
+
* }
|
|
279
|
+
* );
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
async guard(actionType, target, context, fn) {
|
|
283
|
+
const response = await this.decide({
|
|
284
|
+
actionType,
|
|
285
|
+
target,
|
|
286
|
+
context: context ?? void 0
|
|
287
|
+
});
|
|
288
|
+
if (response.decision === "BLOCK") {
|
|
289
|
+
throw new RunplaneError(
|
|
290
|
+
`Action blocked: ${response.reason}`,
|
|
291
|
+
"BLOCKED",
|
|
292
|
+
response.requestId
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (response.decision === "ALLOW") {
|
|
296
|
+
return fn();
|
|
297
|
+
}
|
|
298
|
+
const approval = await this.waitForApproval(response.requestId);
|
|
299
|
+
if (approval.approved) {
|
|
300
|
+
return fn();
|
|
301
|
+
}
|
|
302
|
+
if (approval.status === "denied") {
|
|
303
|
+
throw new RunplaneError(
|
|
304
|
+
`Action denied: ${approval.comment || "No reason provided"}`,
|
|
305
|
+
"DENIED",
|
|
306
|
+
response.requestId
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (this.failMode === "open") {
|
|
310
|
+
return fn();
|
|
311
|
+
}
|
|
312
|
+
throw new RunplaneError(
|
|
313
|
+
"Approval timeout - action blocked (fail-closed)",
|
|
314
|
+
"TIMEOUT",
|
|
315
|
+
response.requestId
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Handle API failures according to failMode
|
|
320
|
+
*/
|
|
321
|
+
handleFailure(reason, request) {
|
|
322
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
323
|
+
if (this.failMode === "open") {
|
|
324
|
+
return {
|
|
325
|
+
decision: "ALLOW",
|
|
326
|
+
reason: `${reason} (fail-open)`,
|
|
327
|
+
requestId
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
decision: "BLOCK",
|
|
332
|
+
reason: `${reason} (fail-closed)`,
|
|
333
|
+
requestId
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
338
|
+
0 && (module.exports = {
|
|
339
|
+
Runplane,
|
|
340
|
+
RunplaneError
|
|
341
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
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
|
+
if (onPoll) {
|
|
50
|
+
onPoll(response.status, elapsed);
|
|
51
|
+
}
|
|
52
|
+
if (response.status !== "pending") {
|
|
53
|
+
return {
|
|
54
|
+
approved: response.status === "approved",
|
|
55
|
+
status: response.status,
|
|
56
|
+
comment: response.comment,
|
|
57
|
+
resolvedBy: response.resolvedBy
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
await this.sleep(interval);
|
|
61
|
+
interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
|
|
62
|
+
} catch {
|
|
63
|
+
await this.sleep(interval);
|
|
64
|
+
interval = Math.min(interval * 2, this.maxPollIntervalMs);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
approved: false,
|
|
69
|
+
status: "expired"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch current approval status from the API
|
|
74
|
+
*/
|
|
75
|
+
async fetchApprovalStatus(requestId) {
|
|
76
|
+
const response = await fetch(
|
|
77
|
+
`${this.baseUrl}/api/approvals/poll/${requestId}`,
|
|
78
|
+
{
|
|
79
|
+
headers: {
|
|
80
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch approval status: ${response.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
return response.json();
|
|
88
|
+
}
|
|
89
|
+
sleep(ms) {
|
|
90
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/client.ts
|
|
95
|
+
var DEFAULT_BASE_URL = "https://runplane.ai";
|
|
96
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
97
|
+
var DEFAULT_FAIL_MODE = "closed";
|
|
98
|
+
var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
99
|
+
var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
|
|
100
|
+
var Runplane = class {
|
|
101
|
+
constructor(config) {
|
|
102
|
+
if (!config.apiKey) {
|
|
103
|
+
throw new RunplaneError(
|
|
104
|
+
"API key is required",
|
|
105
|
+
"INVALID_CONFIG"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
this.apiKey = config.apiKey;
|
|
109
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
110
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
111
|
+
this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
|
|
112
|
+
this.approvalPoller = new ApprovalPoller({
|
|
113
|
+
baseUrl: this.baseUrl,
|
|
114
|
+
apiKey: this.apiKey,
|
|
115
|
+
timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
116
|
+
pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Request a decision from the Runplane control plane.
|
|
121
|
+
*
|
|
122
|
+
* Call this before executing any sensitive action. The response will indicate
|
|
123
|
+
* whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
|
|
124
|
+
*
|
|
125
|
+
* @param request - The action details to evaluate
|
|
126
|
+
* @returns The decision response
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const decision = await runplane.decide({
|
|
131
|
+
* actionType: "delete_record",
|
|
132
|
+
* target: "users.prod",
|
|
133
|
+
* context: { recordId: "usr_123", reason: "gdpr_request" }
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* switch (decision.decision) {
|
|
137
|
+
* case "ALLOW":
|
|
138
|
+
* await deleteRecord(recordId);
|
|
139
|
+
* break;
|
|
140
|
+
* case "BLOCK":
|
|
141
|
+
* console.error("Action blocked:", decision.reason);
|
|
142
|
+
* break;
|
|
143
|
+
* case "REQUIRE_APPROVAL":
|
|
144
|
+
* // Wait for human approval
|
|
145
|
+
* const result = await runplane.waitForApproval(decision.requestId);
|
|
146
|
+
* if (result.approved) {
|
|
147
|
+
* await deleteRecord(recordId);
|
|
148
|
+
* }
|
|
149
|
+
* break;
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
async decide(request) {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`${this.baseUrl}/api/decide`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
agentKey: this.apiKey,
|
|
165
|
+
actionType: request.actionType,
|
|
166
|
+
target: request.target,
|
|
167
|
+
context: request.context ?? {},
|
|
168
|
+
requestId: request.requestId
|
|
169
|
+
}),
|
|
170
|
+
signal: controller.signal
|
|
171
|
+
});
|
|
172
|
+
const json = await response.json();
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return this.handleFailure(
|
|
175
|
+
`API error: ${json.error ?? response.statusText}`,
|
|
176
|
+
request
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
decision: json.decision,
|
|
181
|
+
reason: json.reason,
|
|
182
|
+
requestId: json.requestId,
|
|
183
|
+
matchedRule: json.matchedRule,
|
|
184
|
+
riskScore: json.riskScore,
|
|
185
|
+
severity: json.severity
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
189
|
+
return this.handleFailure(`Network failure: ${message}`, request);
|
|
190
|
+
} finally {
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Wait for an approval decision on a pending request.
|
|
196
|
+
*
|
|
197
|
+
* Use this when decide() returns REQUIRE_APPROVAL. This method will poll
|
|
198
|
+
* the approval endpoint until the request is approved, denied, or times out.
|
|
199
|
+
*
|
|
200
|
+
* @param requestId - The requestId from the decide() response
|
|
201
|
+
* @param options - Optional configuration for polling behavior
|
|
202
|
+
* @returns The approval result
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const decision = await runplane.decide({ ... });
|
|
207
|
+
*
|
|
208
|
+
* if (decision.decision === "REQUIRE_APPROVAL") {
|
|
209
|
+
* console.log("Waiting for approval...");
|
|
210
|
+
*
|
|
211
|
+
* const result = await runplane.waitForApproval(decision.requestId, {
|
|
212
|
+
* timeoutMs: 600000, // 10 minutes
|
|
213
|
+
* onPoll: (status, elapsed) => {
|
|
214
|
+
* console.log(`Still waiting... ${elapsed}ms elapsed`);
|
|
215
|
+
* }
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* if (result.approved) {
|
|
219
|
+
* console.log("Approved by:", result.resolvedBy);
|
|
220
|
+
* } else {
|
|
221
|
+
* console.log("Denied:", result.comment);
|
|
222
|
+
* }
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
async waitForApproval(requestId, options) {
|
|
227
|
+
return this.approvalPoller.poll(requestId, options);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Guard a function with containment evaluation.
|
|
231
|
+
*
|
|
232
|
+
* This is a convenience method that wraps decide() and waitForApproval()
|
|
233
|
+
* into a single call. If the action is blocked or denied, it throws.
|
|
234
|
+
* If approved, it executes the provided function.
|
|
235
|
+
*
|
|
236
|
+
* @param actionType - Type of action being performed
|
|
237
|
+
* @param target - Target resource
|
|
238
|
+
* @param context - Additional context
|
|
239
|
+
* @param fn - Function to execute if allowed
|
|
240
|
+
* @returns The result of fn()
|
|
241
|
+
* @throws RunplaneError if blocked, denied, or times out (in closed mode)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* const result = await runplane.guard(
|
|
246
|
+
* "payment_transfer",
|
|
247
|
+
* "external_bank",
|
|
248
|
+
* { amount: 50000, currency: "USD" },
|
|
249
|
+
* async () => {
|
|
250
|
+
* return await paymentService.transfer(amount, recipient);
|
|
251
|
+
* }
|
|
252
|
+
* );
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async guard(actionType, target, context, fn) {
|
|
256
|
+
const response = await this.decide({
|
|
257
|
+
actionType,
|
|
258
|
+
target,
|
|
259
|
+
context: context ?? void 0
|
|
260
|
+
});
|
|
261
|
+
if (response.decision === "BLOCK") {
|
|
262
|
+
throw new RunplaneError(
|
|
263
|
+
`Action blocked: ${response.reason}`,
|
|
264
|
+
"BLOCKED",
|
|
265
|
+
response.requestId
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (response.decision === "ALLOW") {
|
|
269
|
+
return fn();
|
|
270
|
+
}
|
|
271
|
+
const approval = await this.waitForApproval(response.requestId);
|
|
272
|
+
if (approval.approved) {
|
|
273
|
+
return fn();
|
|
274
|
+
}
|
|
275
|
+
if (approval.status === "denied") {
|
|
276
|
+
throw new RunplaneError(
|
|
277
|
+
`Action denied: ${approval.comment || "No reason provided"}`,
|
|
278
|
+
"DENIED",
|
|
279
|
+
response.requestId
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (this.failMode === "open") {
|
|
283
|
+
return fn();
|
|
284
|
+
}
|
|
285
|
+
throw new RunplaneError(
|
|
286
|
+
"Approval timeout - action blocked (fail-closed)",
|
|
287
|
+
"TIMEOUT",
|
|
288
|
+
response.requestId
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Handle API failures according to failMode
|
|
293
|
+
*/
|
|
294
|
+
handleFailure(reason, request) {
|
|
295
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
296
|
+
if (this.failMode === "open") {
|
|
297
|
+
return {
|
|
298
|
+
decision: "ALLOW",
|
|
299
|
+
reason: `${reason} (fail-open)`,
|
|
300
|
+
requestId
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
decision: "BLOCK",
|
|
305
|
+
reason: `${reason} (fail-closed)`,
|
|
306
|
+
requestId
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
export {
|
|
311
|
+
Runplane,
|
|
312
|
+
RunplaneError
|
|
313
|
+
};
|
package/package.json
CHANGED
|
@@ -1 +1,59 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@runplane/runplane-sdk",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Official SDK for the Runplane control plane - runtime governance for AI agent actions",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
22
|
+
"lint": "eslint src/",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"runplane",
|
|
28
|
+
"ai",
|
|
29
|
+
"agents",
|
|
30
|
+
"governance",
|
|
31
|
+
"security",
|
|
32
|
+
"containment",
|
|
33
|
+
"policy",
|
|
34
|
+
"llm",
|
|
35
|
+
"langchain",
|
|
36
|
+
"openai"
|
|
37
|
+
],
|
|
38
|
+
"author": "Runplane <support@runplane.ai>",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/runplane/sdk.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://runplane.ai/docs",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/runplane/sdk/issues"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"tsup": "^8.0.0",
|
|
54
|
+
"typescript": "^5.0.0",
|
|
55
|
+
"vitest": "^1.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|