@game_ryo/lsji 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
- package/docs/README.md +0 -43
- package/docs/blog/2019-05-28-first-blog-post.mdx +0 -12
- package/docs/blog/2019-05-29-long-blog-post.mdx +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +0 -29
- package/docs/blog/authors.yml +0 -25
- package/docs/blog/tags.yml +0 -19
- package/docs/docs/api/agent.md +0 -151
- package/docs/docs/api/env.md +0 -133
- package/docs/docs/api/environments.md +0 -102
- package/docs/docs/api/qlearning.md +0 -138
- package/docs/docs/api/storage.md +0 -168
- package/docs/docs/architecture.md +0 -155
- package/docs/docs/cli.md +0 -210
- package/docs/docs/contributing.md +0 -162
- package/docs/docs/core-concepts.md +0 -152
- package/docs/docs/examples/advanced-training.md +0 -244
- package/docs/docs/examples/custom-environment.md +0 -198
- package/docs/docs/examples/custom-storage.md +0 -251
- package/docs/docs/getting-started.md +0 -91
- package/docs/docusaurus.config.ts +0 -149
- package/docs/package-lock.json +0 -19522
- package/docs/package.json +0 -49
- package/docs/sidebars.ts +0 -33
- package/docs/src/components/HomepageFeatures/index.tsx +0 -71
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -79
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/index.tsx +0 -44
- package/docs/src/pages/markdown-page.mdx +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docs/tsconfig.json +0 -12
- package/legacy/worker.js +0 -166
- package/legacy/wrangler.toml +0 -11
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL Approval Gate
|
|
3
|
+
*
|
|
4
|
+
* Human-in-the-Loop approval workflow for sensitive operations.
|
|
5
|
+
* Integrates with storage, notifier, and provides timeout/escalation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
import { ApprovalStore, createApprovalStore } from './store.js';
|
|
10
|
+
import { Notifier } from './notifier.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Approval request options
|
|
14
|
+
* @typedef {Object} ApprovalRequest
|
|
15
|
+
* @property {string} action - Action identifier (e.g., 'send_email', 'charge_payment')
|
|
16
|
+
* @property {Object} context - Context data for human review
|
|
17
|
+
* @property {string} requester - Who is requesting approval
|
|
18
|
+
* @property {number} [timeout] - Timeout in ms (default: 5 min)
|
|
19
|
+
* @property {Array<string>} [channels] - Notification channels
|
|
20
|
+
* @property {Object} [metadata] - Additional metadata
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Approval result
|
|
25
|
+
* @typedef {Object} ApprovalResult
|
|
26
|
+
* @property {string} id - Approval ID
|
|
27
|
+
* @property {string} status - 'approved' | 'rejected' | 'expired'
|
|
28
|
+
* @property {string} [decider] - Who decided
|
|
29
|
+
* @property {string} [reason] - Reason for decision
|
|
30
|
+
* @property {Date} decidedAt - When decision was made
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Approval Gate - Manages human approval workflows
|
|
35
|
+
*/
|
|
36
|
+
export class ApprovalGate {
|
|
37
|
+
constructor({ store, notifier, defaultTimeout = 300000 } = {}) {
|
|
38
|
+
this.store = store;
|
|
39
|
+
this.notifier = notifier;
|
|
40
|
+
this.defaultTimeout = defaultTimeout;
|
|
41
|
+
this.pendingApprovals = new Map(); // id -> { resolve, reject, timeout }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Request approval for an action
|
|
46
|
+
* Returns a promise that resolves when approved, rejects when rejected/expired
|
|
47
|
+
*/
|
|
48
|
+
async requestApproval(request) {
|
|
49
|
+
const id = uuidv4();
|
|
50
|
+
const createdAt = new Date();
|
|
51
|
+
const expiresAt = new Date(createdAt.getTime() + (request.timeout || this.defaultTimeout));
|
|
52
|
+
|
|
53
|
+
const approval = {
|
|
54
|
+
id,
|
|
55
|
+
action: request.action,
|
|
56
|
+
context: request.context,
|
|
57
|
+
requester: request.requester,
|
|
58
|
+
status: 'pending',
|
|
59
|
+
createdAt: createdAt.toISOString(),
|
|
60
|
+
expiresAt: expiresAt.toISOString(),
|
|
61
|
+
metadata: request.metadata,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Store in database
|
|
65
|
+
await this.store.createApproval(approval);
|
|
66
|
+
|
|
67
|
+
// Notify via configured channels
|
|
68
|
+
await this.notifier.notify(approval, request.channels);
|
|
69
|
+
|
|
70
|
+
// Return promise that resolves when decision is made
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const timeoutHandle = setTimeout(async () => {
|
|
73
|
+
// Check if still pending (not decided yet)
|
|
74
|
+
const current = await this.store.getApproval(id);
|
|
75
|
+
if (current && current.status === 'pending') {
|
|
76
|
+
await this.store.updateStatus(id, 'expired', null, 'Timeout');
|
|
77
|
+
this.pendingApprovals.delete(id);
|
|
78
|
+
reject(new Error(`Approval timeout: ${id}`));
|
|
79
|
+
}
|
|
80
|
+
}, request.timeout || this.defaultTimeout);
|
|
81
|
+
|
|
82
|
+
this.pendingApprovals.set(id, { resolve, reject, timeout: timeoutHandle });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Approve a pending request
|
|
88
|
+
*/
|
|
89
|
+
async approve(id, { decider, reason } = {}) {
|
|
90
|
+
const result = await this.store.approve(id, { decider, reason });
|
|
91
|
+
|
|
92
|
+
// Resolve pending promise
|
|
93
|
+
const pending = this.pendingApprovals.get(id);
|
|
94
|
+
if (pending) {
|
|
95
|
+
clearTimeout(pending.timeout);
|
|
96
|
+
pending.resolve(result);
|
|
97
|
+
this.pendingApprovals.delete(id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Notify
|
|
101
|
+
await this.notifier.notify({ ...result, status: 'approved' });
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reject a pending request
|
|
108
|
+
*/
|
|
109
|
+
async reject(id, { decider, reason } = {}) {
|
|
110
|
+
const result = await this.store.reject(id, { decider, reason });
|
|
111
|
+
|
|
112
|
+
// Reject pending promise
|
|
113
|
+
const pending = this.pendingApprovals.get(id);
|
|
114
|
+
if (pending) {
|
|
115
|
+
clearTimeout(pending.timeout);
|
|
116
|
+
pending.reject(new Error(`Approval rejected: ${reason || 'No reason provided'}`));
|
|
117
|
+
this.pendingApprovals.delete(id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Notify
|
|
121
|
+
await this.notifier.notify({ ...result, status: 'rejected' });
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get approval status
|
|
128
|
+
*/
|
|
129
|
+
async getApproval(id) {
|
|
130
|
+
return this.store.getApproval(id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all pending approvals
|
|
135
|
+
*/
|
|
136
|
+
async getPendingApprovals(limit = 100) {
|
|
137
|
+
return this.store.getPendingApprovals(limit);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get approvals by requester
|
|
142
|
+
*/
|
|
143
|
+
async getApprovalsByRequester(requester, limit = 100) {
|
|
144
|
+
return this.store.getApprovalsByRequester(requester, limit);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Clean up expired approvals
|
|
149
|
+
*/
|
|
150
|
+
async cleanup() {
|
|
151
|
+
await this.store.cleanupExpired();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if action requires approval
|
|
156
|
+
* Override this to define custom approval rules
|
|
157
|
+
*/
|
|
158
|
+
requiresApproval(action, context = {}) {
|
|
159
|
+
// Default: require approval for sensitive actions
|
|
160
|
+
const sensitiveActions = [
|
|
161
|
+
'send_email',
|
|
162
|
+
'charge_payment',
|
|
163
|
+
'delete_data',
|
|
164
|
+
'modify_user',
|
|
165
|
+
'deploy_code',
|
|
166
|
+
'run_sql',
|
|
167
|
+
'api_call',
|
|
168
|
+
'file_write',
|
|
169
|
+
'ssh_command',
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
return sensitiveActions.includes(action);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute action with automatic approval if needed
|
|
177
|
+
*/
|
|
178
|
+
async executeWithApproval(action, context, executor, requester = 'agent') {
|
|
179
|
+
if (!this.requiresApproval(action, context)) {
|
|
180
|
+
return executor();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Request approval
|
|
184
|
+
const approval = await this.requestApproval({
|
|
185
|
+
action,
|
|
186
|
+
context,
|
|
187
|
+
requester,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (approval.status === 'approved') {
|
|
191
|
+
return executor();
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Action ${action} was ${approval.status}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Create approval gate from config
|
|
200
|
+
*/
|
|
201
|
+
export async function createApprovalGate(config = {}) {
|
|
202
|
+
const store = await createApprovalStore(config.store || {});
|
|
203
|
+
const notifier = new Notifier(config.notifier || {});
|
|
204
|
+
|
|
205
|
+
return new ApprovalGate({
|
|
206
|
+
store,
|
|
207
|
+
notifier,
|
|
208
|
+
defaultTimeout: config.defaultTimeout,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL (Human-in-the-Loop) System
|
|
3
|
+
*
|
|
4
|
+
* Exports all HITL components:
|
|
5
|
+
* - ApprovalStore: Persistent storage for approvals
|
|
6
|
+
* - Notifier: Multi-channel notifications
|
|
7
|
+
* - ApprovalGate: Approval workflow management
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { ApprovalStore, createApprovalStore } from './store.js';
|
|
11
|
+
export { Notifier, NotificationChannel, createNotifier } from './notifier.js';
|
|
12
|
+
export { ApprovalGate, createApprovalGate } from './approval-gate.js';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL Notifier
|
|
3
|
+
*
|
|
4
|
+
* Multi-channel notification system for approval requests.
|
|
5
|
+
* Supports console, webhook, email, and custom handlers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Notification channels
|
|
10
|
+
*/
|
|
11
|
+
export const NotificationChannel = {
|
|
12
|
+
CONSOLE: 'console',
|
|
13
|
+
WEBHOOK: 'webhook',
|
|
14
|
+
EMAIL: 'email',
|
|
15
|
+
CUSTOM: 'custom',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Notifier - Sends approval notifications via multiple channels
|
|
20
|
+
*/
|
|
21
|
+
export class Notifier {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.channels = new Map();
|
|
24
|
+
this.defaultChannels = config.defaultChannels || [NotificationChannel.CONSOLE];
|
|
25
|
+
|
|
26
|
+
// Register built-in channels
|
|
27
|
+
this.registerChannel(NotificationChannel.CONSOLE, this.notifyConsole.bind(this));
|
|
28
|
+
|
|
29
|
+
if (config.webhookUrl) {
|
|
30
|
+
this.registerChannel(NotificationChannel.WEBHOOK, this.notifyWebhook.bind(this, config.webhookUrl));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (config.emailConfig) {
|
|
34
|
+
this.registerChannel(NotificationChannel.EMAIL, this.notifyEmail.bind(this, config.emailConfig));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Register custom channels
|
|
38
|
+
if (config.customChannels) {
|
|
39
|
+
for (const [name, handler] of Object.entries(config.customChannels)) {
|
|
40
|
+
this.registerChannel(name, handler);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a notification channel
|
|
47
|
+
*/
|
|
48
|
+
registerChannel(name, handler) {
|
|
49
|
+
this.channels.set(name, handler);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unregister a channel
|
|
54
|
+
*/
|
|
55
|
+
unregisterChannel(name) {
|
|
56
|
+
this.channels.delete(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send notification to all configured channels
|
|
61
|
+
*/
|
|
62
|
+
async notify(approval, channels = this.defaultChannels) {
|
|
63
|
+
const results = [];
|
|
64
|
+
|
|
65
|
+
for (const channel of channels) {
|
|
66
|
+
const handler = this.channels.get(channel);
|
|
67
|
+
if (handler) {
|
|
68
|
+
try {
|
|
69
|
+
await handler(approval);
|
|
70
|
+
results.push({ channel, success: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
results.push({ channel, success: false, error: error.message });
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
results.push({ channel, success: false, error: 'Channel not registered' });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Console notification (built-in)
|
|
84
|
+
*/
|
|
85
|
+
async notifyConsole(approval) {
|
|
86
|
+
const status = approval.status === 'pending' ? '⏳ PENDING' :
|
|
87
|
+
approval.status === 'approved' ? '✅ APPROVED' :
|
|
88
|
+
approval.status === 'rejected' ? '❌ REJECTED' : '⏰ EXPIRED';
|
|
89
|
+
|
|
90
|
+
console.log(`
|
|
91
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
92
|
+
║ HITL Approval ${status.padEnd(50)}║
|
|
93
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
94
|
+
║ ID: ${approval.id.padEnd(50)}║
|
|
95
|
+
║ Action: ${approval.action.padEnd(50)}║
|
|
96
|
+
║ Requester: ${approval.requester.padEnd(50)}║
|
|
97
|
+
║ Status: ${status.padEnd(50)}║
|
|
98
|
+
║ Created: ${new Date(approval.createdAt).toLocaleString().padEnd(50)}║
|
|
99
|
+
${approval.expiresAt ? `║ Expires: ${new Date(approval.expiresAt).toLocaleString().padEnd(50)}║` : ''}
|
|
100
|
+
${approval.context ? `║ Context: ${JSON.stringify(approval.context).substring(0, 48).padEnd(50)}║` : ''}
|
|
101
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Webhook notification (built-in)
|
|
107
|
+
*/
|
|
108
|
+
async notifyWebhook(webhookUrl, approval) {
|
|
109
|
+
const payload = {
|
|
110
|
+
type: 'hitl_approval',
|
|
111
|
+
approval: {
|
|
112
|
+
id: approval.id,
|
|
113
|
+
action: approval.action,
|
|
114
|
+
context: approval.context,
|
|
115
|
+
requester: approval.requester,
|
|
116
|
+
status: approval.status,
|
|
117
|
+
createdAt: approval.createdAt,
|
|
118
|
+
expiresAt: approval.expiresAt,
|
|
119
|
+
},
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const response = await fetch(webhookUrl, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify(payload),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Email notification (built-in - requires email service)
|
|
136
|
+
*/
|
|
137
|
+
async notifyEmail(emailConfig, approval) {
|
|
138
|
+
// This is a placeholder - integrate with actual email service
|
|
139
|
+
// Example: SendGrid, Mailgun, AWS SES, etc.
|
|
140
|
+
console.log(`[EMAIL] Would send approval notification to ${emailConfig.to}`);
|
|
141
|
+
console.log(`Subject: [LSJI] Approval ${approval.status}: ${approval.action}`);
|
|
142
|
+
console.log(`Body: Approval ${approval.id} requires your attention`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create notifier from config
|
|
148
|
+
*/
|
|
149
|
+
export function createNotifier(config = {}) {
|
|
150
|
+
return new Notifier(config);
|
|
151
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval Store
|
|
3
|
+
*
|
|
4
|
+
* Persistent storage for approval requests and decisions.
|
|
5
|
+
* Uses the existing storage abstraction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createStorage } from '../../index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Approval request record
|
|
12
|
+
* @typedef {Object} ApprovalRecord
|
|
13
|
+
* @property {string} id - Unique approval ID
|
|
14
|
+
* @property {string} action - Action requiring approval
|
|
15
|
+
* @property {Object} context - Context data for the action
|
|
16
|
+
* @property {string} requester - Who requested the approval
|
|
17
|
+
* @property {string} status - 'pending' | 'approved' | 'rejected' | 'expired'
|
|
18
|
+
* @property {Date} createdAt - When request was created
|
|
19
|
+
* @property {Date} [expiresAt] - When request expires
|
|
20
|
+
* @property {Date} [decidedAt] - When decision was made
|
|
21
|
+
* @property {string} [decider] - Who made the decision
|
|
22
|
+
* @property {string} [reason] - Reason for approval/rejection
|
|
23
|
+
* @property {Object} [metadata] - Additional metadata
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Approval Store - Manages approval requests in storage
|
|
28
|
+
*/
|
|
29
|
+
export class ApprovalStore {
|
|
30
|
+
constructor(storage) {
|
|
31
|
+
this.storage = storage;
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if storage is SQL-based (has db with all method)
|
|
37
|
+
*/
|
|
38
|
+
isSqlStorage() {
|
|
39
|
+
return this.storage.db && typeof this.storage.db.all === 'function';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize the approval table
|
|
44
|
+
*/
|
|
45
|
+
async initialize() {
|
|
46
|
+
if (this.initialized) return;
|
|
47
|
+
|
|
48
|
+
// Create approvals table if using SQL storage
|
|
49
|
+
if (this.isSqlStorage()) {
|
|
50
|
+
await this.storage.db.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
52
|
+
id TEXT PRIMARY KEY,
|
|
53
|
+
action TEXT NOT NULL,
|
|
54
|
+
context TEXT NOT NULL,
|
|
55
|
+
requester TEXT NOT NULL,
|
|
56
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
57
|
+
created_at TEXT NOT NULL,
|
|
58
|
+
expires_at TEXT,
|
|
59
|
+
decided_at TEXT,
|
|
60
|
+
decider TEXT,
|
|
61
|
+
reason TEXT,
|
|
62
|
+
metadata TEXT
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
// Create index for pending approvals
|
|
67
|
+
await this.storage.db.exec(`
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status)
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
await this.storage.db.exec(`
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_created ON approvals(created_at)
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.initialized = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new approval request
|
|
81
|
+
*/
|
|
82
|
+
async createApproval(approval) {
|
|
83
|
+
await this.initialize();
|
|
84
|
+
|
|
85
|
+
const record = {
|
|
86
|
+
id: approval.id,
|
|
87
|
+
action: approval.action,
|
|
88
|
+
context: JSON.stringify(approval.context),
|
|
89
|
+
requester: approval.requester,
|
|
90
|
+
status: 'pending',
|
|
91
|
+
createdAt: approval.createdAt || new Date().toISOString(),
|
|
92
|
+
expiresAt: approval.expiresAt?.toISOString() || null,
|
|
93
|
+
decidedAt: null,
|
|
94
|
+
decider: null,
|
|
95
|
+
reason: null,
|
|
96
|
+
metadata: approval.metadata ? JSON.stringify(approval.metadata) : null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (this.isSqlStorage()) {
|
|
100
|
+
await this.storage.db.run(
|
|
101
|
+
`INSERT INTO approvals (id, action, context, requester, status, created_at, expires_at, decided_at, decider, reason, metadata)
|
|
102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
103
|
+
[record.id, record.action, record.context, record.requester, record.status,
|
|
104
|
+
record.createdAt, record.expiresAt, record.decidedAt, record.decider, record.reason, record.metadata]
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
// Fallback for memory storage
|
|
108
|
+
if (!this.memoryApprovals) this.memoryApprovals = new Map();
|
|
109
|
+
this.memoryApprovals.set(record.id, record);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get approval by ID
|
|
117
|
+
*/
|
|
118
|
+
async getApproval(id) {
|
|
119
|
+
await this.initialize();
|
|
120
|
+
|
|
121
|
+
if (this.isSqlStorage()) {
|
|
122
|
+
const row = await this.storage.db.get('SELECT * FROM approvals WHERE id = ?', [id]);
|
|
123
|
+
if (!row) return null;
|
|
124
|
+
return this.rowToRecord(row);
|
|
125
|
+
} else {
|
|
126
|
+
return this.memoryApprovals?.get(id) || null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get pending approvals
|
|
132
|
+
*/
|
|
133
|
+
async getPendingApprovals(limit = 100) {
|
|
134
|
+
await this.initialize();
|
|
135
|
+
|
|
136
|
+
if (this.isSqlStorage()) {
|
|
137
|
+
const rows = await this.storage.db.all(
|
|
138
|
+
'SELECT * FROM approvals WHERE status = ? ORDER BY created_at DESC LIMIT ?',
|
|
139
|
+
['pending', limit]
|
|
140
|
+
);
|
|
141
|
+
return rows.map(r => this.rowToRecord(r));
|
|
142
|
+
} else {
|
|
143
|
+
const approvals = [];
|
|
144
|
+
for (const record of this.memoryApprovals?.values() || []) {
|
|
145
|
+
if (record.status === 'pending') {
|
|
146
|
+
approvals.push(record);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return approvals.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, limit);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get approvals by requester
|
|
155
|
+
*/
|
|
156
|
+
async getApprovalsByRequester(requester, limit = 100) {
|
|
157
|
+
await this.initialize();
|
|
158
|
+
|
|
159
|
+
if (this.isSqlStorage()) {
|
|
160
|
+
const rows = await this.storage.db.all(
|
|
161
|
+
'SELECT * FROM approvals WHERE requester = ? ORDER BY created_at DESC LIMIT ?',
|
|
162
|
+
[requester, limit]
|
|
163
|
+
);
|
|
164
|
+
return rows.map(r => this.rowToRecord(r));
|
|
165
|
+
} else {
|
|
166
|
+
const approvals = [];
|
|
167
|
+
for (const record of this.memoryApprovals?.values() || []) {
|
|
168
|
+
if (record.requester === requester) {
|
|
169
|
+
approvals.push(record);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return approvals.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).slice(0, limit);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update approval status (approve/reject)
|
|
178
|
+
*/
|
|
179
|
+
async decideApproval(id, { status, decider, reason }) {
|
|
180
|
+
await this.initialize();
|
|
181
|
+
|
|
182
|
+
const validStatuses = ['approved', 'rejected'];
|
|
183
|
+
if (!validStatuses.includes(status)) {
|
|
184
|
+
throw new Error(`Invalid status: ${status}. Must be 'approved' or 'rejected'`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const record = await this.getApproval(id);
|
|
188
|
+
if (!record) {
|
|
189
|
+
throw new Error(`Approval not found: ${id}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (record.status !== 'pending') {
|
|
193
|
+
throw new Error(`Approval already decided: ${record.status}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check expiration
|
|
197
|
+
if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
|
|
198
|
+
await this.updateStatus(id, 'expired', null, 'Expired');
|
|
199
|
+
throw new Error('Approval request has expired');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const decidedAt = new Date().toISOString();
|
|
203
|
+
|
|
204
|
+
if (this.isSqlStorage()) {
|
|
205
|
+
await this.storage.db.run(
|
|
206
|
+
`UPDATE approvals SET status = ?, decided_at = ?, decider = ?, reason = ? WHERE id = ?`,
|
|
207
|
+
[status, decidedAt, decider, reason, id]
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
record.status = status;
|
|
211
|
+
record.decidedAt = decidedAt;
|
|
212
|
+
record.decider = decider;
|
|
213
|
+
record.reason = reason;
|
|
214
|
+
this.memoryApprovals.set(id, record);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { ...record, status, decidedAt, decider, reason };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Approve a request
|
|
222
|
+
*/
|
|
223
|
+
async approve(id, { decider, reason } = {}) {
|
|
224
|
+
return this.decideApproval(id, { status: 'approved', decider, reason });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Reject a request
|
|
229
|
+
*/
|
|
230
|
+
async reject(id, { decider, reason } = {}) {
|
|
231
|
+
return this.decideApproval(id, { status: 'rejected', decider, reason });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update status (internal)
|
|
236
|
+
*/
|
|
237
|
+
async updateStatus(id, status, decider = null, reason = null) {
|
|
238
|
+
await this.initialize();
|
|
239
|
+
|
|
240
|
+
if (this.isSqlStorage()) {
|
|
241
|
+
await this.storage.db.run(
|
|
242
|
+
`UPDATE approvals SET status = ?, decided_at = ?, decider = ?, reason = ? WHERE id = ?`,
|
|
243
|
+
[status, new Date().toISOString(), decider, reason, id]
|
|
244
|
+
);
|
|
245
|
+
} else {
|
|
246
|
+
const record = this.memoryApprovals?.get(id);
|
|
247
|
+
if (record) {
|
|
248
|
+
record.status = status;
|
|
249
|
+
record.decidedAt = new Date().toISOString();
|
|
250
|
+
record.decider = decider;
|
|
251
|
+
record.reason = reason;
|
|
252
|
+
this.memoryApprovals.set(id, record);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clean up expired approvals
|
|
259
|
+
*/
|
|
260
|
+
async cleanupExpired() {
|
|
261
|
+
await this.initialize();
|
|
262
|
+
|
|
263
|
+
const now = new Date().toISOString();
|
|
264
|
+
|
|
265
|
+
if (this.isSqlStorage()) {
|
|
266
|
+
await this.storage.db.run(
|
|
267
|
+
`UPDATE approvals SET status = 'expired' WHERE status = 'pending' AND expires_at < ?`,
|
|
268
|
+
[now]
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
for (const record of this.memoryApprovals?.values() || []) {
|
|
272
|
+
if (record.status === 'pending' && record.expiresAt && new Date(record.expiresAt) < new Date()) {
|
|
273
|
+
record.status = 'expired';
|
|
274
|
+
this.memoryApprovals.set(record.id, record);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Convert database row to record
|
|
282
|
+
*/
|
|
283
|
+
rowToRecord(row) {
|
|
284
|
+
return {
|
|
285
|
+
id: row.id,
|
|
286
|
+
action: row.action,
|
|
287
|
+
context: JSON.parse(row.context),
|
|
288
|
+
requester: row.requester,
|
|
289
|
+
status: row.status,
|
|
290
|
+
createdAt: row.created_at,
|
|
291
|
+
expiresAt: row.expires_at,
|
|
292
|
+
decidedAt: row.decided_at,
|
|
293
|
+
decider: row.decider,
|
|
294
|
+
reason: row.reason,
|
|
295
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create approval store from storage config
|
|
302
|
+
*/
|
|
303
|
+
export async function createApprovalStore(storageConfig = {}) {
|
|
304
|
+
const storage = await createStorage(
|
|
305
|
+
storageConfig.type || 'sqlite',
|
|
306
|
+
storageConfig.options || {}
|
|
307
|
+
);
|
|
308
|
+
const store = new ApprovalStore(storage);
|
|
309
|
+
await store.initialize();
|
|
310
|
+
return store;
|
|
311
|
+
}
|