@shaifulshabuj-waymarks/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/server.js +218 -0
- package/dist/approvals/handler.js +77 -0
- package/dist/db/database.js +209 -0
- package/dist/mcp/server.js +331 -0
- package/dist/notifications/slack.js +77 -0
- package/dist/policies/engine.js +161 -0
- package/package.json +58 -0
- package/src/ui/index.html +418 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
require("dotenv/config");
|
|
40
|
+
const express_1 = __importDefault(require("express"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const database_1 = require("../db/database");
|
|
44
|
+
const engine_1 = require("../policies/engine");
|
|
45
|
+
const handler_1 = require("../approvals/handler");
|
|
46
|
+
const app = (0, express_1.default)();
|
|
47
|
+
const PORT = 3001;
|
|
48
|
+
app.use(express_1.default.json());
|
|
49
|
+
app.use(express_1.default.urlencoded({ extended: true }));
|
|
50
|
+
// Serve UI — path works for both ts-node (src/api/) and compiled (dist/api/)
|
|
51
|
+
const UI_DIR = path.resolve(__dirname, '../../src/ui');
|
|
52
|
+
app.use(express_1.default.static(UI_DIR));
|
|
53
|
+
// GET /api/actions — list all actions (or ?count=true for pending count)
|
|
54
|
+
app.get('/api/actions', (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
if (req.query.count === 'true') {
|
|
57
|
+
return res.json({ count: (0, database_1.getPendingCount)() });
|
|
58
|
+
}
|
|
59
|
+
const actions = (0, database_1.getActions)();
|
|
60
|
+
res.json(actions);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
res.status(500).json({ error: err.message });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// POST /api/actions/:action_id/approve
|
|
67
|
+
app.post('/api/actions/:action_id/approve', async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await (0, handler_1.approvePendingAction)(req.params.action_id, 'ui');
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
const status = result.error === 'Action not found' ? 404 : 400;
|
|
72
|
+
return res.status(status).json({ error: result.error });
|
|
73
|
+
}
|
|
74
|
+
res.json(result);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
res.status(500).json({ error: err.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// POST /api/actions/:action_id/reject
|
|
81
|
+
app.post('/api/actions/:action_id/reject', async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const reason = req.body?.reason || 'Rejected';
|
|
84
|
+
const result = await (0, handler_1.rejectPendingAction)(req.params.action_id, reason);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
const status = result.error === 'Action not found' ? 404 : 400;
|
|
87
|
+
return res.status(status).json({ error: result.error });
|
|
88
|
+
}
|
|
89
|
+
res.json(result);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
res.status(500).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// GET /api/actions/:action_id/status — lightweight status for agent polling
|
|
96
|
+
app.get('/api/actions/:action_id/status', (req, res) => {
|
|
97
|
+
try {
|
|
98
|
+
const action = (0, database_1.getAction)(req.params.action_id);
|
|
99
|
+
if (!action) {
|
|
100
|
+
return res.status(404).json({ error: 'Action not found' });
|
|
101
|
+
}
|
|
102
|
+
res.json({
|
|
103
|
+
status: action.status,
|
|
104
|
+
decision: action.decision,
|
|
105
|
+
approved_by: action.approved_by,
|
|
106
|
+
approved_at: action.approved_at,
|
|
107
|
+
rejected_reason: action.rejected_reason,
|
|
108
|
+
rejected_at: action.rejected_at,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
res.status(500).json({ error: err.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// GET /api/actions/:action_id — single action
|
|
116
|
+
app.get('/api/actions/:action_id', (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const action = (0, database_1.getAction)(req.params.action_id);
|
|
119
|
+
if (!action) {
|
|
120
|
+
return res.status(404).json({ error: 'Action not found' });
|
|
121
|
+
}
|
|
122
|
+
res.json(action);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
res.status(500).json({ error: err.message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// POST /api/actions/:action_id/rollback
|
|
129
|
+
app.post('/api/actions/:action_id/rollback', (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const action = (0, database_1.getAction)(req.params.action_id);
|
|
132
|
+
if (!action) {
|
|
133
|
+
return res.status(404).json({ error: 'Action not found' });
|
|
134
|
+
}
|
|
135
|
+
if (action.tool_name !== 'write_file') {
|
|
136
|
+
return res.status(400).json({ error: 'Rollback only supported for write_file actions' });
|
|
137
|
+
}
|
|
138
|
+
if (action.rolled_back) {
|
|
139
|
+
return res.status(400).json({ error: 'Action already rolled back' });
|
|
140
|
+
}
|
|
141
|
+
if (!action.target_path) {
|
|
142
|
+
return res.status(400).json({ error: 'No target path on this action' });
|
|
143
|
+
}
|
|
144
|
+
// Bug 3: if no before_snapshot, file was newly created — delete it
|
|
145
|
+
if (!action.before_snapshot) {
|
|
146
|
+
fs.unlinkSync(action.target_path);
|
|
147
|
+
(0, database_1.markRolledBack)(action.action_id);
|
|
148
|
+
return res.json({ success: true, action: 'deleted', message: `Deleted new file: ${action.target_path}` });
|
|
149
|
+
}
|
|
150
|
+
// Restore file to before_snapshot
|
|
151
|
+
fs.mkdirSync(path.dirname(action.target_path), { recursive: true });
|
|
152
|
+
fs.writeFileSync(action.target_path, action.before_snapshot, 'utf8');
|
|
153
|
+
(0, database_1.markRolledBack)(action.action_id);
|
|
154
|
+
res.json({ success: true, action: 'restored', message: `Restored ${action.target_path} to previous state` });
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
res.status(500).json({ error: err.message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// POST /api/slack/interact — Slack interactive components (button clicks)
|
|
161
|
+
// For local development: use ngrok or similar to expose this endpoint publicly: ngrok http 3001
|
|
162
|
+
app.post('/api/slack/interact', async (req, res) => {
|
|
163
|
+
let payload;
|
|
164
|
+
try {
|
|
165
|
+
payload = JSON.parse(req.body.payload);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.error('Slack interact parse error:', err);
|
|
169
|
+
return res.status(400).json({ error: 'Invalid payload format' });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
if (!payload?.actions?.[0]) {
|
|
173
|
+
return res.status(400).json({ error: 'No actions in payload' });
|
|
174
|
+
}
|
|
175
|
+
const slackAction = payload.actions[0];
|
|
176
|
+
const actionId = slackAction.action_id;
|
|
177
|
+
const actionValue = slackAction.value; // waymark action_id
|
|
178
|
+
if (actionId === 'waymark_approve') {
|
|
179
|
+
const result = await (0, handler_1.approvePendingAction)(actionValue, 'slack');
|
|
180
|
+
return res.json({ text: result.success ? '✅ Approved by slack' : `❌ Error: ${result.error}` });
|
|
181
|
+
}
|
|
182
|
+
if (actionId === 'waymark_reject') {
|
|
183
|
+
const result = await (0, handler_1.rejectPendingAction)(actionValue, 'Rejected via Slack');
|
|
184
|
+
return res.json({ text: result.success ? '❌ Rejected by slack' : `❌ Error: ${result.error}` });
|
|
185
|
+
}
|
|
186
|
+
res.status(400).json({ error: `Unknown action_id: ${actionId}` });
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
res.status(500).json({ error: err.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// GET /api/sessions
|
|
193
|
+
app.get('/api/sessions', (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const sessions = (0, database_1.getSessions)();
|
|
196
|
+
res.json(sessions);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
res.status(500).json({ error: err.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// GET /api/config
|
|
203
|
+
app.get('/api/config', (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
res.json((0, engine_1.loadConfig)());
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
res.status(500).json({ error: err.message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Fallback: serve UI for any unmatched route
|
|
212
|
+
app.get('*', (req, res) => {
|
|
213
|
+
res.sendFile(path.join(UI_DIR, 'index.html'));
|
|
214
|
+
});
|
|
215
|
+
app.listen(PORT, () => {
|
|
216
|
+
console.log(`Waymark UI + API running at http://localhost:${PORT}`);
|
|
217
|
+
});
|
|
218
|
+
exports.default = app;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.approvePendingAction = approvePendingAction;
|
|
37
|
+
exports.rejectPendingAction = rejectPendingAction;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const database_1 = require("../db/database");
|
|
41
|
+
async function approvePendingAction(actionId, approvedBy = 'ui') {
|
|
42
|
+
const action = (0, database_1.getAction)(actionId);
|
|
43
|
+
if (!action) {
|
|
44
|
+
return { success: false, error: 'Action not found' };
|
|
45
|
+
}
|
|
46
|
+
if (action.status !== 'pending') {
|
|
47
|
+
return { success: false, error: `Action is not pending (current status: ${action.status})` };
|
|
48
|
+
}
|
|
49
|
+
let after_snapshot;
|
|
50
|
+
if (action.tool_name === 'write_file') {
|
|
51
|
+
const { path: filePath, content } = JSON.parse(action.input_payload);
|
|
52
|
+
const resolvedPath = path.resolve(filePath);
|
|
53
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
54
|
+
fs.writeFileSync(resolvedPath, content, 'utf8');
|
|
55
|
+
after_snapshot = content;
|
|
56
|
+
}
|
|
57
|
+
else if (action.tool_name === 'read_file') {
|
|
58
|
+
// read_file: no re-execution needed, just mark approved
|
|
59
|
+
// (the file read already happened conceptually; approval means "yes, this was ok")
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return { success: false, error: `Unsupported tool for approval: ${action.tool_name}` };
|
|
63
|
+
}
|
|
64
|
+
(0, database_1.approveAction)(actionId, approvedBy, after_snapshot);
|
|
65
|
+
return { success: true, action: actionId };
|
|
66
|
+
}
|
|
67
|
+
async function rejectPendingAction(actionId, reason) {
|
|
68
|
+
const action = (0, database_1.getAction)(actionId);
|
|
69
|
+
if (!action) {
|
|
70
|
+
return { success: false, error: 'Action not found' };
|
|
71
|
+
}
|
|
72
|
+
if (action.status !== 'pending') {
|
|
73
|
+
return { success: false, error: `Action is not pending (current status: ${action.status})` };
|
|
74
|
+
}
|
|
75
|
+
(0, database_1.rejectAction)(actionId, reason);
|
|
76
|
+
return { success: true, action: actionId };
|
|
77
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.insertAction = insertAction;
|
|
40
|
+
exports.updateAction = updateAction;
|
|
41
|
+
exports.getActions = getActions;
|
|
42
|
+
exports.getAction = getAction;
|
|
43
|
+
exports.markRolledBack = markRolledBack;
|
|
44
|
+
exports.getSessions = getSessions;
|
|
45
|
+
exports.approveAction = approveAction;
|
|
46
|
+
exports.rejectAction = rejectAction;
|
|
47
|
+
exports.getPendingCount = getPendingCount;
|
|
48
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
52
|
+
const DATA_DIR = path.join(PROJECT_ROOT, 'data');
|
|
53
|
+
const DB_PATH = path.join(DATA_DIR, 'waymark.db');
|
|
54
|
+
// Ensure data directory exists
|
|
55
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
56
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
const db = new better_sqlite3_1.default(DB_PATH);
|
|
59
|
+
// Create table on startup
|
|
60
|
+
db.exec(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS action_log (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
action_id TEXT UNIQUE NOT NULL,
|
|
64
|
+
session_id TEXT NOT NULL,
|
|
65
|
+
tool_name TEXT NOT NULL,
|
|
66
|
+
target_path TEXT,
|
|
67
|
+
input_payload TEXT NOT NULL,
|
|
68
|
+
before_snapshot TEXT,
|
|
69
|
+
after_snapshot TEXT,
|
|
70
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
71
|
+
error_message TEXT,
|
|
72
|
+
stdout TEXT,
|
|
73
|
+
stderr TEXT,
|
|
74
|
+
rolled_back INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
rolled_back_at TEXT,
|
|
76
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
// Migrate existing DBs — add stdout/stderr if not present
|
|
80
|
+
try {
|
|
81
|
+
db.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
try {
|
|
85
|
+
db.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
// Migrate v2: policy engine columns
|
|
89
|
+
try {
|
|
90
|
+
db.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
try {
|
|
94
|
+
db.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
try {
|
|
98
|
+
db.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
|
|
99
|
+
}
|
|
100
|
+
catch { }
|
|
101
|
+
// Migrate v3: approval flow columns
|
|
102
|
+
try {
|
|
103
|
+
db.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
|
|
104
|
+
}
|
|
105
|
+
catch { }
|
|
106
|
+
try {
|
|
107
|
+
db.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
try {
|
|
111
|
+
db.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
try {
|
|
115
|
+
db.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
const insertStmt = db.prepare(`
|
|
119
|
+
INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule)
|
|
120
|
+
VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule)
|
|
121
|
+
`);
|
|
122
|
+
const updateStmt = db.prepare(`
|
|
123
|
+
UPDATE action_log
|
|
124
|
+
SET status = COALESCE(@status, status),
|
|
125
|
+
after_snapshot = COALESCE(@after_snapshot, after_snapshot),
|
|
126
|
+
error_message = COALESCE(@error_message, error_message),
|
|
127
|
+
stdout = COALESCE(@stdout, stdout),
|
|
128
|
+
stderr = COALESCE(@stderr, stderr)
|
|
129
|
+
WHERE action_id = @action_id
|
|
130
|
+
`);
|
|
131
|
+
function insertAction(params) {
|
|
132
|
+
insertStmt.run({
|
|
133
|
+
action_id: params.action_id,
|
|
134
|
+
session_id: params.session_id,
|
|
135
|
+
tool_name: params.tool_name,
|
|
136
|
+
target_path: params.target_path ?? null,
|
|
137
|
+
input_payload: params.input_payload,
|
|
138
|
+
before_snapshot: params.before_snapshot ?? null,
|
|
139
|
+
status: params.status,
|
|
140
|
+
decision: params.decision ?? 'allow',
|
|
141
|
+
policy_reason: params.policy_reason ?? null,
|
|
142
|
+
matched_rule: params.matched_rule ?? null,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function updateAction(action_id, params) {
|
|
146
|
+
updateStmt.run({
|
|
147
|
+
action_id,
|
|
148
|
+
status: params.status ?? null,
|
|
149
|
+
after_snapshot: params.after_snapshot ?? null,
|
|
150
|
+
error_message: params.error_message ?? null,
|
|
151
|
+
stdout: params.stdout ?? null,
|
|
152
|
+
stderr: params.stderr ?? null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function getActions() {
|
|
156
|
+
return db.prepare(`
|
|
157
|
+
SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
|
|
158
|
+
`).all();
|
|
159
|
+
}
|
|
160
|
+
function getAction(action_id) {
|
|
161
|
+
return db.prepare(`
|
|
162
|
+
SELECT * FROM action_log WHERE action_id = ?
|
|
163
|
+
`).get(action_id);
|
|
164
|
+
}
|
|
165
|
+
function markRolledBack(action_id) {
|
|
166
|
+
db.prepare(`
|
|
167
|
+
UPDATE action_log
|
|
168
|
+
SET rolled_back = 1, rolled_back_at = datetime('now')
|
|
169
|
+
WHERE action_id = ?
|
|
170
|
+
`).run(action_id);
|
|
171
|
+
}
|
|
172
|
+
function getSessions() {
|
|
173
|
+
return db.prepare(`
|
|
174
|
+
SELECT session_id,
|
|
175
|
+
COUNT(*) as action_count,
|
|
176
|
+
MAX(created_at) as latest
|
|
177
|
+
FROM action_log
|
|
178
|
+
GROUP BY session_id
|
|
179
|
+
ORDER BY latest DESC
|
|
180
|
+
`).all();
|
|
181
|
+
}
|
|
182
|
+
function approveAction(action_id, approved_by, after_snapshot) {
|
|
183
|
+
db.prepare(`
|
|
184
|
+
UPDATE action_log
|
|
185
|
+
SET status = 'success',
|
|
186
|
+
decision = 'allow',
|
|
187
|
+
approved_at = datetime('now'),
|
|
188
|
+
approved_by = @approved_by,
|
|
189
|
+
after_snapshot = COALESCE(@after_snapshot, after_snapshot)
|
|
190
|
+
WHERE action_id = @action_id
|
|
191
|
+
`).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
|
|
192
|
+
}
|
|
193
|
+
function rejectAction(action_id, reason) {
|
|
194
|
+
db.prepare(`
|
|
195
|
+
UPDATE action_log
|
|
196
|
+
SET status = 'rejected',
|
|
197
|
+
decision = 'rejected',
|
|
198
|
+
rejected_at = datetime('now'),
|
|
199
|
+
rejected_reason = @reason
|
|
200
|
+
WHERE action_id = @action_id
|
|
201
|
+
`).run({ action_id, reason });
|
|
202
|
+
}
|
|
203
|
+
function getPendingCount() {
|
|
204
|
+
const row = db.prepare(`
|
|
205
|
+
SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
|
|
206
|
+
`).get();
|
|
207
|
+
return row.count;
|
|
208
|
+
}
|
|
209
|
+
exports.default = db;
|