@olane/o-approval 0.7.7
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 +272 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/interfaces/approval-config.d.ts +12 -0
- package/dist/src/interfaces/approval-config.d.ts.map +1 -0
- package/dist/src/interfaces/approval-config.js +1 -0
- package/dist/src/interfaces/approval-request.d.ts +8 -0
- package/dist/src/interfaces/approval-request.d.ts.map +1 -0
- package/dist/src/interfaces/approval-request.js +1 -0
- package/dist/src/interfaces/approval-response.d.ts +7 -0
- package/dist/src/interfaces/approval-response.d.ts.map +1 -0
- package/dist/src/interfaces/approval-response.js +1 -0
- package/dist/src/interfaces/index.d.ts +4 -0
- package/dist/src/interfaces/index.d.ts.map +1 -0
- package/dist/src/interfaces/index.js +3 -0
- package/dist/src/methods/approval.methods.d.ts +5 -0
- package/dist/src/methods/approval.methods.d.ts.map +1 -0
- package/dist/src/methods/approval.methods.js +71 -0
- package/dist/src/o-approval.tool.d.ts +50 -0
- package/dist/src/o-approval.tool.d.ts.map +1 -0
- package/dist/src/o-approval.tool.js +304 -0
- package/dist/test/method.spec.d.ts +1 -0
- package/dist/test/method.spec.d.ts.map +1 -0
- package/dist/test/method.spec.js +29 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# @olane/o-approval
|
|
2
|
+
|
|
3
|
+
Human-in-the-loop approval service for AI actions in Olane OS.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `o-approval` package provides a configurable approval system that allows humans to review and approve AI actions before they are executed. This ensures that sensitive operations require human oversight, making Olane OS suitable for production environments where AI autonomy needs to be controlled.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Configurable Modes**: Switch between `allow`, `review`, and `auto` modes
|
|
12
|
+
- **Preference Management**: Whitelist/blacklist specific tool methods
|
|
13
|
+
- **Timeout Protection**: Auto-denies actions after configurable timeout (default: 3 minutes)
|
|
14
|
+
- **Persistent Preferences**: "Always allow" and "never allow" choices are saved
|
|
15
|
+
- **Backward Compatible**: Defaults to `allow` mode for seamless integration
|
|
16
|
+
- **Non-Invasive**: Single interception point at task execution level
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
This package is typically included as part of the Olane OS common tools and is automatically registered on all nodes.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @olane/o-approval
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### CLI Commands
|
|
29
|
+
|
|
30
|
+
Enable review mode (all AI actions require approval):
|
|
31
|
+
```bash
|
|
32
|
+
o config set approvalMode=review
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Disable review mode (default - no approval required):
|
|
36
|
+
```bash
|
|
37
|
+
o config set approvalMode=allow
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Enable auto mode (only methods marked with `requiresApproval: true` need approval):
|
|
41
|
+
```bash
|
|
42
|
+
o config set approvalMode=auto
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Check current mode:
|
|
46
|
+
```bash
|
|
47
|
+
o config get approvalMode
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
View approval mode in status:
|
|
51
|
+
```bash
|
|
52
|
+
o status
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Programmatic Usage
|
|
56
|
+
|
|
57
|
+
#### Initialize Approval Tool
|
|
58
|
+
|
|
59
|
+
The approval tool is automatically registered by `initCommonTools()`, but you can also create it manually:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { oApprovalTool } from '@olane/o-approval';
|
|
63
|
+
|
|
64
|
+
const approvalTool = new oApprovalTool({
|
|
65
|
+
name: 'approval',
|
|
66
|
+
parent: parentNode.address,
|
|
67
|
+
leader: leaderNode.address,
|
|
68
|
+
mode: 'review', // 'allow' | 'review' | 'auto'
|
|
69
|
+
preferences: {
|
|
70
|
+
whitelist: ['o://storage/get', 'o://intelligence/prompt'],
|
|
71
|
+
blacklist: ['o://storage/delete'],
|
|
72
|
+
timeout: 180000, // 3 minutes in milliseconds
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await approvalTool.start();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### Request Approval
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { oAddress } from '@olane/o-core';
|
|
83
|
+
|
|
84
|
+
const response = await node.use(new oAddress('o://approval'), {
|
|
85
|
+
method: 'request_approval',
|
|
86
|
+
params: {
|
|
87
|
+
toolAddress: 'o://storage',
|
|
88
|
+
method: 'delete_file',
|
|
89
|
+
params: { file_path: '/important/document.txt' },
|
|
90
|
+
intent: 'Delete the old configuration file',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (response.result.data.approved) {
|
|
95
|
+
// Proceed with action
|
|
96
|
+
console.log('Action approved');
|
|
97
|
+
} else {
|
|
98
|
+
console.log('Action denied:', response.result.data.decision);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Approval Flow
|
|
103
|
+
|
|
104
|
+
When an AI action requires approval, the human receives a prompt like this:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Action requires approval:
|
|
108
|
+
Tool: o://storage
|
|
109
|
+
Method: delete_file
|
|
110
|
+
Parameters: {
|
|
111
|
+
"file_path": "/important/document.txt"
|
|
112
|
+
}
|
|
113
|
+
Intent: Clean up old configuration files
|
|
114
|
+
|
|
115
|
+
Response options:
|
|
116
|
+
- 'approve' - Allow this action once
|
|
117
|
+
- 'deny' - Reject this action
|
|
118
|
+
- 'always' - Always allow o://storage/delete_file
|
|
119
|
+
- 'never' - Never allow o://storage/delete_file
|
|
120
|
+
|
|
121
|
+
Your response:
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Human Responses
|
|
125
|
+
|
|
126
|
+
- **`approve`**: Allows the action this one time
|
|
127
|
+
- **`deny`**: Rejects the action (AI receives an error)
|
|
128
|
+
- **`always`**: Adds the tool/method to the whitelist (auto-approves in future)
|
|
129
|
+
- **`never`**: Adds the tool/method to the blacklist (auto-denies in future)
|
|
130
|
+
|
|
131
|
+
## Approval Modes
|
|
132
|
+
|
|
133
|
+
### `allow` Mode (Default)
|
|
134
|
+
- No approval required
|
|
135
|
+
- All AI actions execute automatically
|
|
136
|
+
- Backward compatible with existing Olane OS instances
|
|
137
|
+
|
|
138
|
+
### `review` Mode
|
|
139
|
+
- All AI actions require human approval
|
|
140
|
+
- Every tool execution is intercepted
|
|
141
|
+
- Provides maximum human oversight
|
|
142
|
+
|
|
143
|
+
### `auto` Mode
|
|
144
|
+
- Only methods marked with `requiresApproval: true` need approval
|
|
145
|
+
- Allows fine-grained control at the method level
|
|
146
|
+
- Best for production environments with trusted tools
|
|
147
|
+
|
|
148
|
+
## API Reference
|
|
149
|
+
|
|
150
|
+
### Methods
|
|
151
|
+
|
|
152
|
+
#### `request_approval`
|
|
153
|
+
|
|
154
|
+
Request human approval for an AI action.
|
|
155
|
+
|
|
156
|
+
**Parameters:**
|
|
157
|
+
- `toolAddress` (string, required): The address of the tool to be called
|
|
158
|
+
- `method` (string, required): The method name to be called
|
|
159
|
+
- `params` (object, required): The parameters for the method call
|
|
160
|
+
- `intent` (string, optional): The original intent that triggered this action
|
|
161
|
+
|
|
162
|
+
**Returns:**
|
|
163
|
+
```typescript
|
|
164
|
+
{
|
|
165
|
+
success: boolean;
|
|
166
|
+
approved: boolean;
|
|
167
|
+
decision: 'approve' | 'deny' | 'always' | 'never';
|
|
168
|
+
timestamp: number;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### `set_preference`
|
|
173
|
+
|
|
174
|
+
Store an approval preference (whitelist/blacklist).
|
|
175
|
+
|
|
176
|
+
**Parameters:**
|
|
177
|
+
- `toolMethod` (string, required): The tool/method combination (e.g., "o://storage/delete")
|
|
178
|
+
- `preference` (string, required): The preference type: "allow" or "deny"
|
|
179
|
+
|
|
180
|
+
**Returns:**
|
|
181
|
+
```typescript
|
|
182
|
+
{
|
|
183
|
+
success: boolean;
|
|
184
|
+
message: string;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### `get_mode`
|
|
189
|
+
|
|
190
|
+
Get the current approval mode.
|
|
191
|
+
|
|
192
|
+
**Parameters:** None
|
|
193
|
+
|
|
194
|
+
**Returns:**
|
|
195
|
+
```typescript
|
|
196
|
+
{
|
|
197
|
+
success: boolean;
|
|
198
|
+
mode: 'allow' | 'review' | 'auto';
|
|
199
|
+
preferences: ApprovalPreferences;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### `set_mode`
|
|
204
|
+
|
|
205
|
+
Set the approval mode.
|
|
206
|
+
|
|
207
|
+
**Parameters:**
|
|
208
|
+
- `mode` (string, required): The approval mode: "allow", "review", or "auto"
|
|
209
|
+
|
|
210
|
+
**Returns:**
|
|
211
|
+
```typescript
|
|
212
|
+
{
|
|
213
|
+
success: boolean;
|
|
214
|
+
mode: 'allow' | 'review' | 'auto';
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
### Marking Methods as Requiring Approval
|
|
221
|
+
|
|
222
|
+
To mark a method as requiring approval in `auto` mode, add the `requiresApproval` flag:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { oMethod } from '@olane/o-protocol';
|
|
226
|
+
|
|
227
|
+
export const MY_METHODS: { [key: string]: oMethod } = {
|
|
228
|
+
delete_file: {
|
|
229
|
+
name: 'delete_file',
|
|
230
|
+
description: 'Delete a file from storage',
|
|
231
|
+
parameters: [...],
|
|
232
|
+
dependencies: [],
|
|
233
|
+
requiresApproval: true, // Requires approval in auto mode
|
|
234
|
+
approvalMetadata: {
|
|
235
|
+
riskLevel: 'high',
|
|
236
|
+
category: 'destructive',
|
|
237
|
+
description: 'Permanently deletes a file',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Security Considerations
|
|
244
|
+
|
|
245
|
+
### Defense in Depth
|
|
246
|
+
|
|
247
|
+
The approval system provides Layer 4 in the Olane OS security architecture:
|
|
248
|
+
|
|
249
|
+
1. **Layer 1 (Database)**: RLS policies prevent unauthorized data access
|
|
250
|
+
2. **Layer 2 (Application)**: Caller validation ensures proper ownership
|
|
251
|
+
3. **Layer 3 (Audit)**: Access logging tracks all operations
|
|
252
|
+
4. **Layer 4 (Human-in-the-Loop)**: Approval system prevents unauthorized AI actions
|
|
253
|
+
|
|
254
|
+
### Best Practices
|
|
255
|
+
|
|
256
|
+
1. **Use `review` mode in production** for sensitive environments
|
|
257
|
+
2. **Use `auto` mode** with carefully marked methods for trusted environments
|
|
258
|
+
3. **Regularly review audit logs** for denied actions
|
|
259
|
+
4. **Keep whitelist/blacklist minimal** to avoid approval fatigue
|
|
260
|
+
5. **Set appropriate timeouts** based on response time expectations
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
(MIT OR Apache-2.0)
|
|
265
|
+
|
|
266
|
+
## Related Packages
|
|
267
|
+
|
|
268
|
+
- [@olane/o-core](../o-core) - Core node functionality
|
|
269
|
+
- [@olane/o-lane](../o-lane) - Lane execution and capabilities
|
|
270
|
+
- [@olane/o-login](../o-login) - Human/AI agent authentication
|
|
271
|
+
- [@olane/o-protocol](../o-protocol) - Protocol definitions
|
|
272
|
+
- [@olane/o-tools-common](../o-tools-common) - Common tools initialization
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { oNodeToolConfig } from '@olane/o-node';
|
|
2
|
+
export type ApprovalMode = 'allow' | 'review' | 'auto';
|
|
3
|
+
export interface ApprovalPreferences {
|
|
4
|
+
whitelist?: string[];
|
|
5
|
+
blacklist?: string[];
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface oApprovalConfig extends oNodeToolConfig {
|
|
9
|
+
mode?: ApprovalMode;
|
|
10
|
+
preferences?: ApprovalPreferences;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=approval-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-config.d.ts","sourceRoot":"","sources":["../../../src/interfaces/approval-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-request.d.ts","sourceRoot":"","sources":["../../../src/interfaces/approval-request.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-response.d.ts","sourceRoot":"","sources":["../../../src/interfaces/approval-response.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interfaces/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval.methods.d.ts","sourceRoot":"","sources":["../../../src/methods/approval.methods.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,eAAO,MAAM,gBAAgB,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAsEtD,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const APPROVAL_METHODS = {
|
|
2
|
+
request_approval: {
|
|
3
|
+
name: 'request_approval',
|
|
4
|
+
description: 'Request human approval for an AI action',
|
|
5
|
+
parameters: [
|
|
6
|
+
{
|
|
7
|
+
name: 'toolAddress',
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'The address of the tool to be called',
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'method',
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'The method name to be called',
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'params',
|
|
20
|
+
type: 'object',
|
|
21
|
+
description: 'The parameters for the method call',
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'intent',
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'The original intent that triggered this action',
|
|
28
|
+
required: false,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
dependencies: [],
|
|
32
|
+
},
|
|
33
|
+
set_preference: {
|
|
34
|
+
name: 'set_preference',
|
|
35
|
+
description: 'Store an approval preference (whitelist/blacklist)',
|
|
36
|
+
parameters: [
|
|
37
|
+
{
|
|
38
|
+
name: 'toolMethod',
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The tool/method combination (e.g., "o://storage/delete")',
|
|
41
|
+
required: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'preference',
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'The preference type: "allow" or "deny"',
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
dependencies: [],
|
|
51
|
+
},
|
|
52
|
+
get_mode: {
|
|
53
|
+
name: 'get_mode',
|
|
54
|
+
description: 'Get the current approval mode',
|
|
55
|
+
parameters: [],
|
|
56
|
+
dependencies: [],
|
|
57
|
+
},
|
|
58
|
+
set_mode: {
|
|
59
|
+
name: 'set_mode',
|
|
60
|
+
description: 'Set the approval mode',
|
|
61
|
+
parameters: [
|
|
62
|
+
{
|
|
63
|
+
name: 'mode',
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'The approval mode: "allow", "review", or "auto"',
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
dependencies: [],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ToolResult } from '@olane/o-tool';
|
|
2
|
+
import { oRequest } from '@olane/o-core';
|
|
3
|
+
import { oApprovalConfig } from './interfaces/approval-config.js';
|
|
4
|
+
import { oLaneTool } from '@olane/o-lane';
|
|
5
|
+
export declare class oApprovalTool extends oLaneTool {
|
|
6
|
+
private mode;
|
|
7
|
+
private preferences;
|
|
8
|
+
constructor(config: oApprovalConfig);
|
|
9
|
+
/**
|
|
10
|
+
* Check if approval is needed for a given tool/method combination
|
|
11
|
+
*/
|
|
12
|
+
private needsApproval;
|
|
13
|
+
/**
|
|
14
|
+
* Request human approval for an action
|
|
15
|
+
*/
|
|
16
|
+
_tool_request_approval(request: oRequest): Promise<ToolResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Format the approval prompt for the human
|
|
19
|
+
*/
|
|
20
|
+
private formatApprovalPrompt;
|
|
21
|
+
/**
|
|
22
|
+
* Parse the human's approval response
|
|
23
|
+
*/
|
|
24
|
+
private parseApprovalResponse;
|
|
25
|
+
/**
|
|
26
|
+
* Add a tool/method to the whitelist
|
|
27
|
+
*/
|
|
28
|
+
private addToWhitelist;
|
|
29
|
+
/**
|
|
30
|
+
* Add a tool/method to the blacklist
|
|
31
|
+
*/
|
|
32
|
+
private addToBlacklist;
|
|
33
|
+
/**
|
|
34
|
+
* Save preferences to OS config
|
|
35
|
+
*/
|
|
36
|
+
private savePreferences;
|
|
37
|
+
/**
|
|
38
|
+
* Set an approval preference manually
|
|
39
|
+
*/
|
|
40
|
+
_tool_set_preference(request: oRequest): Promise<ToolResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Get the current approval mode
|
|
43
|
+
*/
|
|
44
|
+
_tool_get_mode(request: oRequest): Promise<ToolResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Set the approval mode
|
|
47
|
+
*/
|
|
48
|
+
_tool_set_mode(request: oRequest): Promise<ToolResult>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=o-approval.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"o-approval.tool.d.ts","sourceRoot":"","sources":["../../src/o-approval.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAY,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EACL,eAAe,EAGhB,MAAM,iCAAiC,CAAC;AAMzC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAK1C,qBAAa,aAAc,SAAQ,SAAS;IAC1C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,WAAW,CAAsB;gBAE7B,MAAM,EAAE,eAAe;IAenC;;OAEG;IACH,OAAO,CAAC,aAAa;IA4BrB;;OAEG;IACG,sBAAsB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAuHpE;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmB5B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkB7B;;OAEG;YACW,cAAc;IAU5B;;OAEG;YACW,cAAc;IAU5B;;OAEG;YACW,eAAe;IAY7B;;OAEG;IACG,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IA+BlE;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;IAQ5D;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC;CA+B7D"}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { oAddress } from '@olane/o-core';
|
|
2
|
+
import { APPROVAL_METHODS } from './methods/approval.methods.js';
|
|
3
|
+
import { oLaneTool } from '@olane/o-lane';
|
|
4
|
+
import { oNodeAddress } from '@olane/o-node';
|
|
5
|
+
const DEFAULT_TIMEOUT = 180000; // 3 minutes in milliseconds
|
|
6
|
+
export class oApprovalTool extends oLaneTool {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super({
|
|
9
|
+
...config,
|
|
10
|
+
address: new oNodeAddress('o://approval'),
|
|
11
|
+
description: 'Human approval service for AI actions',
|
|
12
|
+
methods: APPROVAL_METHODS,
|
|
13
|
+
});
|
|
14
|
+
this.mode = config.mode || 'allow';
|
|
15
|
+
this.preferences = config.preferences || {
|
|
16
|
+
whitelist: [],
|
|
17
|
+
blacklist: [],
|
|
18
|
+
timeout: DEFAULT_TIMEOUT,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if approval is needed for a given tool/method combination
|
|
23
|
+
*/
|
|
24
|
+
needsApproval(toolAddress, method) {
|
|
25
|
+
const toolMethod = `${toolAddress}/${method}`;
|
|
26
|
+
// Check blacklist first - always deny
|
|
27
|
+
if (this.preferences.blacklist?.includes(toolMethod)) {
|
|
28
|
+
return false; // Will be denied, but doesn't need approval prompt
|
|
29
|
+
}
|
|
30
|
+
// Check whitelist - always allow
|
|
31
|
+
if (this.preferences.whitelist?.includes(toolMethod)) {
|
|
32
|
+
return false; // Pre-approved
|
|
33
|
+
}
|
|
34
|
+
// Determine based on mode
|
|
35
|
+
switch (this.mode) {
|
|
36
|
+
case 'allow':
|
|
37
|
+
return false; // No approval needed
|
|
38
|
+
case 'review':
|
|
39
|
+
return true; // Always require approval
|
|
40
|
+
case 'auto':
|
|
41
|
+
// In auto mode, only methods marked with requiresApproval need approval
|
|
42
|
+
// This will be checked by the caller
|
|
43
|
+
return true;
|
|
44
|
+
default:
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Request human approval for an action
|
|
50
|
+
*/
|
|
51
|
+
async _tool_request_approval(request) {
|
|
52
|
+
try {
|
|
53
|
+
const toolAddress = request.params.toolAddress;
|
|
54
|
+
const method = request.params.method;
|
|
55
|
+
const params = request.params.params;
|
|
56
|
+
const intent = request.params.intent;
|
|
57
|
+
const approvalRequest = {
|
|
58
|
+
toolAddress,
|
|
59
|
+
method,
|
|
60
|
+
params,
|
|
61
|
+
intent,
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
const toolMethod = `${toolAddress}/${method}`;
|
|
65
|
+
// Check if this action is blacklisted
|
|
66
|
+
if (this.preferences.blacklist?.includes(toolMethod)) {
|
|
67
|
+
this.logger.warn(`Action denied by blacklist: ${toolMethod}`);
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: 'Action denied by approval blacklist',
|
|
71
|
+
approved: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Check if this action is whitelisted
|
|
75
|
+
if (this.preferences.whitelist?.includes(toolMethod)) {
|
|
76
|
+
this.logger.info(`Action pre-approved by whitelist: ${toolMethod}`);
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
approved: true,
|
|
80
|
+
decision: 'approve',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Check if approval is needed based on mode
|
|
84
|
+
if (!this.needsApproval(toolAddress, method)) {
|
|
85
|
+
this.logger.debug(`Approval not required for ${toolMethod} (mode: ${this.mode})`);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
approved: true,
|
|
89
|
+
decision: 'approve',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Format the approval prompt
|
|
93
|
+
const question = this.formatApprovalPrompt(approvalRequest);
|
|
94
|
+
// Request approval from human
|
|
95
|
+
this.logger.info(`Requesting approval for: ${toolMethod}`);
|
|
96
|
+
const timeout = this.preferences.timeout || DEFAULT_TIMEOUT;
|
|
97
|
+
const approvalPromise = this.use(new oAddress('o://human'), {
|
|
98
|
+
method: 'question',
|
|
99
|
+
params: { question },
|
|
100
|
+
});
|
|
101
|
+
// Implement timeout
|
|
102
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Approval timeout')), timeout));
|
|
103
|
+
try {
|
|
104
|
+
const response = await Promise.race([approvalPromise, timeoutPromise]);
|
|
105
|
+
const answer = response.result.data?.answer
|
|
106
|
+
?.trim()
|
|
107
|
+
.toLowerCase();
|
|
108
|
+
// Parse the response
|
|
109
|
+
const decision = this.parseApprovalResponse(answer);
|
|
110
|
+
// Handle preference storage
|
|
111
|
+
if (decision === 'always') {
|
|
112
|
+
await this.addToWhitelist(toolMethod);
|
|
113
|
+
this.logger.info(`Added to whitelist: ${toolMethod}`);
|
|
114
|
+
}
|
|
115
|
+
else if (decision === 'never') {
|
|
116
|
+
await this.addToBlacklist(toolMethod);
|
|
117
|
+
this.logger.info(`Added to blacklist: ${toolMethod}`);
|
|
118
|
+
}
|
|
119
|
+
const approved = decision === 'approve' || decision === 'always';
|
|
120
|
+
this.logger.info(`Approval decision for ${toolMethod}: ${decision} (approved: ${approved})`);
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
approved,
|
|
124
|
+
decision,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// Timeout or error - default to deny
|
|
130
|
+
this.logger.error(`Approval timeout or error for ${toolMethod}:`, error.message);
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
error: error.message || 'Approval timeout',
|
|
134
|
+
approved: false,
|
|
135
|
+
decision: 'deny',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.logger.error('Error in approval request:', error);
|
|
141
|
+
return {
|
|
142
|
+
success: false,
|
|
143
|
+
error: error.message || 'Unknown error',
|
|
144
|
+
approved: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format the approval prompt for the human
|
|
150
|
+
*/
|
|
151
|
+
formatApprovalPrompt(request) {
|
|
152
|
+
const paramsStr = JSON.stringify(request.params, null, 2);
|
|
153
|
+
return `
|
|
154
|
+
Action requires approval:
|
|
155
|
+
Tool: ${request.toolAddress}
|
|
156
|
+
Method: ${request.method}
|
|
157
|
+
Parameters: ${paramsStr}
|
|
158
|
+
${request.intent ? `Intent: ${request.intent}` : ''}
|
|
159
|
+
|
|
160
|
+
Response options:
|
|
161
|
+
- 'approve' - Allow this action once
|
|
162
|
+
- 'deny' - Reject this action
|
|
163
|
+
- 'always' - Always allow ${request.toolAddress}/${request.method}
|
|
164
|
+
- 'never' - Never allow ${request.toolAddress}/${request.method}
|
|
165
|
+
|
|
166
|
+
Your response:`.trim();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse the human's approval response
|
|
170
|
+
*/
|
|
171
|
+
parseApprovalResponse(answer) {
|
|
172
|
+
if (!answer)
|
|
173
|
+
return 'deny';
|
|
174
|
+
if (answer.includes('approve') ||
|
|
175
|
+
answer.includes('yes') ||
|
|
176
|
+
answer.includes('allow')) {
|
|
177
|
+
return 'approve';
|
|
178
|
+
}
|
|
179
|
+
else if (answer.includes('always')) {
|
|
180
|
+
return 'always';
|
|
181
|
+
}
|
|
182
|
+
else if (answer.includes('never')) {
|
|
183
|
+
return 'never';
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return 'deny';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Add a tool/method to the whitelist
|
|
191
|
+
*/
|
|
192
|
+
async addToWhitelist(toolMethod) {
|
|
193
|
+
if (!this.preferences.whitelist) {
|
|
194
|
+
this.preferences.whitelist = [];
|
|
195
|
+
}
|
|
196
|
+
if (!this.preferences.whitelist.includes(toolMethod)) {
|
|
197
|
+
this.preferences.whitelist.push(toolMethod);
|
|
198
|
+
await this.savePreferences();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Add a tool/method to the blacklist
|
|
203
|
+
*/
|
|
204
|
+
async addToBlacklist(toolMethod) {
|
|
205
|
+
if (!this.preferences.blacklist) {
|
|
206
|
+
this.preferences.blacklist = [];
|
|
207
|
+
}
|
|
208
|
+
if (!this.preferences.blacklist.includes(toolMethod)) {
|
|
209
|
+
this.preferences.blacklist.push(toolMethod);
|
|
210
|
+
await this.savePreferences();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Save preferences to OS config
|
|
215
|
+
*/
|
|
216
|
+
async savePreferences() {
|
|
217
|
+
try {
|
|
218
|
+
// Save preferences via the leader node's config
|
|
219
|
+
await this.use(new oAddress('o://leader'), {
|
|
220
|
+
method: 'update_approval_preferences',
|
|
221
|
+
params: { preferences: this.preferences },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
this.logger.error('Failed to save approval preferences:', error.message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Set an approval preference manually
|
|
230
|
+
*/
|
|
231
|
+
async _tool_set_preference(request) {
|
|
232
|
+
try {
|
|
233
|
+
const toolMethod = request.params.toolMethod;
|
|
234
|
+
const preference = request.params.preference;
|
|
235
|
+
if (preference === 'allow') {
|
|
236
|
+
await this.addToWhitelist(toolMethod);
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
message: `Added ${toolMethod} to whitelist`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
else if (preference === 'deny') {
|
|
243
|
+
await this.addToBlacklist(toolMethod);
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
message: `Added ${toolMethod} to blacklist`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
return {
|
|
251
|
+
success: false,
|
|
252
|
+
error: 'Invalid preference. Use "allow" or "deny"',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: error.message || 'Unknown error',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the current approval mode
|
|
265
|
+
*/
|
|
266
|
+
async _tool_get_mode(request) {
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
mode: this.mode,
|
|
270
|
+
preferences: this.preferences,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Set the approval mode
|
|
275
|
+
*/
|
|
276
|
+
async _tool_set_mode(request) {
|
|
277
|
+
try {
|
|
278
|
+
const mode = request.params.mode;
|
|
279
|
+
if (!['allow', 'review', 'auto'].includes(mode)) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: 'Invalid mode. Use "allow", "review", or "auto"',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
this.mode = mode;
|
|
286
|
+
this.logger.info(`Approval mode set to: ${this.mode}`);
|
|
287
|
+
// Save mode to OS config
|
|
288
|
+
await this.use(new oAddress('o://leader'), {
|
|
289
|
+
method: 'update_approval_mode',
|
|
290
|
+
params: { mode: this.mode },
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
success: true,
|
|
294
|
+
mode: this.mode,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
error: error.message || 'Unknown error',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=method.spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"method.spec.d.ts","sourceRoot":"","sources":["../../test/method.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// import { oAddress, NodeState, oRequest } from '@olane/o-core';
|
|
3
|
+
// import { oVirtualTool } from '../src/virtual.tool.js';
|
|
4
|
+
// import { expect } from 'chai';
|
|
5
|
+
// describe('o-tool @methods', () => {
|
|
6
|
+
// it('should call the hello_world method', async () => {
|
|
7
|
+
// const node = new oVirtualTool({
|
|
8
|
+
// address: new oAddress('o://test'),
|
|
9
|
+
// leader: null,
|
|
10
|
+
// parent: null,
|
|
11
|
+
// });
|
|
12
|
+
// await node.start();
|
|
13
|
+
// expect(node.state).to.equal(NodeState.RUNNING);
|
|
14
|
+
// // call the tool
|
|
15
|
+
// const req = new oRequest({
|
|
16
|
+
// method: 'hello_world',
|
|
17
|
+
// id: '123',
|
|
18
|
+
// params: {
|
|
19
|
+
// _connectionId: '123',
|
|
20
|
+
// _requestMethod: 'hello_world',
|
|
21
|
+
// },
|
|
22
|
+
// });
|
|
23
|
+
// const data = await node.callMyTool(req);
|
|
24
|
+
// expect(data.message).to.equal('Hello, world!');
|
|
25
|
+
// // stop the node
|
|
26
|
+
// await node.stop();
|
|
27
|
+
// expect(node.state).to.equal(NodeState.STOPPED);
|
|
28
|
+
// });
|
|
29
|
+
// });
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@olane/o-approval",
|
|
3
|
+
"version": "0.7.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/src/index.d.ts",
|
|
10
|
+
"default": "./dist/src/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/**/*",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "aegir test",
|
|
20
|
+
"test:node": "aegir test -t node",
|
|
21
|
+
"test:browser": "aegir test -t browser",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"deep:clean": "rm -rf node_modules && rm package-lock.json",
|
|
24
|
+
"start:prod": "node dist/index.js",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"update:lib": "npm install @olane/o-core@latest",
|
|
27
|
+
"update:peers": "npm install @olane/o-config@latest @olane/o-core@latest @olane/o-protocol@latest @olane/o-tool@latest @olane/o-lane@latest --save-peer",
|
|
28
|
+
"lint": "eslint src/**/*.ts"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/olane-labs/olane.git"
|
|
33
|
+
},
|
|
34
|
+
"author": "oLane Inc.",
|
|
35
|
+
"license": "(MIT OR Apache-2.0)",
|
|
36
|
+
"description": "Olane approval service for human-in-the-loop AI action approval",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
39
|
+
"@eslint/js": "^9.29.0",
|
|
40
|
+
"@tsconfig/node20": "^20.1.6",
|
|
41
|
+
"@types/jest": "^30.0.0",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
|
43
|
+
"@typescript-eslint/parser": "^8.34.1",
|
|
44
|
+
"aegir": "^47.0.21",
|
|
45
|
+
"eslint": "^9.29.0",
|
|
46
|
+
"eslint-config-prettier": "^10.1.6",
|
|
47
|
+
"eslint-plugin-prettier": "^5.5.0",
|
|
48
|
+
"globals": "^16.2.0",
|
|
49
|
+
"jest": "^30.0.0",
|
|
50
|
+
"prettier": "^3.5.3",
|
|
51
|
+
"ts-jest": "^29.4.0",
|
|
52
|
+
"ts-node": "^10.9.2",
|
|
53
|
+
"tsconfig-paths": "^4.2.0",
|
|
54
|
+
"tsx": "^4.20.3",
|
|
55
|
+
"typescript": "5.4.5"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@olane/o-config": "^0.7.6",
|
|
59
|
+
"@olane/o-core": "^0.7.6",
|
|
60
|
+
"@olane/o-lane": "^0.7.6",
|
|
61
|
+
"@olane/o-protocol": "^0.7.6",
|
|
62
|
+
"@olane/o-tool": "^0.7.6"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"debug": "^4.4.1",
|
|
66
|
+
"dotenv": "^16.5.0"
|
|
67
|
+
}
|
|
68
|
+
}
|