@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 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,4 @@
1
+ export * from './o-approval.tool.js';
2
+ export * from './interfaces/index.js';
3
+ export * from './methods/approval.methods.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './o-approval.tool.js';
2
+ export * from './interfaces/index.js';
3
+ export * from './methods/approval.methods.js';
@@ -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,8 @@
1
+ export interface ApprovalRequest {
2
+ toolAddress: string;
3
+ method: string;
4
+ params: any;
5
+ intent?: string;
6
+ timestamp: number;
7
+ }
8
+ //# sourceMappingURL=approval-request.d.ts.map
@@ -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,7 @@
1
+ export type ApprovalDecision = 'approve' | 'deny' | 'always' | 'never';
2
+ export interface ApprovalResponse {
3
+ decision: ApprovalDecision;
4
+ reason?: string;
5
+ timestamp: number;
6
+ }
7
+ //# sourceMappingURL=approval-response.d.ts.map
@@ -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,4 @@
1
+ export * from './approval-request.js';
2
+ export * from './approval-response.js';
3
+ export * from './approval-config.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './approval-request.js';
2
+ export * from './approval-response.js';
3
+ export * from './approval-config.js';
@@ -0,0 +1,5 @@
1
+ import { oMethod } from '@olane/o-protocol';
2
+ export declare const APPROVAL_METHODS: {
3
+ [key: string]: oMethod;
4
+ };
5
+ //# sourceMappingURL=approval.methods.d.ts.map
@@ -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
+ }