@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,331 @@
|
|
|
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
|
+
// Parse --project-root arg before any imports trigger module-level DB/config initialization
|
|
37
|
+
const _projectRootIdx = process.argv.indexOf('--project-root');
|
|
38
|
+
if (_projectRootIdx !== -1 && process.argv[_projectRootIdx + 1]) {
|
|
39
|
+
process.env.WAYMARK_PROJECT_ROOT = process.argv[_projectRootIdx + 1];
|
|
40
|
+
}
|
|
41
|
+
require("dotenv/config");
|
|
42
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
43
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
44
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const uuid_1 = require("uuid");
|
|
49
|
+
const database_1 = require("../db/database");
|
|
50
|
+
const engine_1 = require("../policies/engine");
|
|
51
|
+
const slack_1 = require("../notifications/slack");
|
|
52
|
+
const SESSION_ID = (0, uuid_1.v4)();
|
|
53
|
+
// Bug 1: Build PATH that includes nvm-managed node binaries.
|
|
54
|
+
// Sourcing shell profiles non-interactively is unreliable (NVM_DIR unset, brew missing, etc.)
|
|
55
|
+
// Instead: read nvm's default alias directly from ~/.nvm and append known bin paths.
|
|
56
|
+
function getUserPath() {
|
|
57
|
+
const base = process.env.PATH || '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
|
|
58
|
+
const extra = [];
|
|
59
|
+
const home = process.env.HOME || require('os').homedir();
|
|
60
|
+
const nvmDir = process.env.NVM_DIR || path.join(home, '.nvm');
|
|
61
|
+
// Add all nvm version bin dirs that exist, default alias first
|
|
62
|
+
try {
|
|
63
|
+
const defaultAlias = path.join(nvmDir, 'alias', 'default');
|
|
64
|
+
if (fs.existsSync(defaultAlias)) {
|
|
65
|
+
const ver = fs.readFileSync(defaultAlias, 'utf8').trim().replace(/^v/, '');
|
|
66
|
+
const binDir = path.join(nvmDir, 'versions', 'node', `v${ver}`, 'bin');
|
|
67
|
+
if (fs.existsSync(binDir))
|
|
68
|
+
extra.push(binDir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
// Also add common fixed locations as fallback
|
|
73
|
+
for (const p of ['/usr/local/bin', '/opt/homebrew/bin']) {
|
|
74
|
+
if (fs.existsSync(p) && !base.includes(p))
|
|
75
|
+
extra.push(p);
|
|
76
|
+
}
|
|
77
|
+
return extra.length ? `${extra.join(':')}:${base}` : base;
|
|
78
|
+
}
|
|
79
|
+
const USER_PATH = getUserPath();
|
|
80
|
+
const server = new index_js_1.Server({ name: 'waymark', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
81
|
+
// List available tools
|
|
82
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
83
|
+
return {
|
|
84
|
+
tools: [
|
|
85
|
+
{
|
|
86
|
+
name: 'write_file',
|
|
87
|
+
description: 'Write content to a file. Creates or overwrites the file.',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
path: { type: 'string', description: 'Absolute or relative path to the file' },
|
|
92
|
+
content: { type: 'string', description: 'Content to write to the file' },
|
|
93
|
+
},
|
|
94
|
+
required: ['path', 'content'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'read_file',
|
|
99
|
+
description: 'Read content from a file.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
path: { type: 'string', description: 'Absolute or relative path to the file' },
|
|
104
|
+
},
|
|
105
|
+
required: ['path'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'bash',
|
|
110
|
+
description: 'Execute a bash command and return output.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
command: { type: 'string', description: 'The bash command to execute' },
|
|
115
|
+
},
|
|
116
|
+
required: ['command'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
// Handle tool calls
|
|
123
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
124
|
+
const { name, arguments: args } = request.params;
|
|
125
|
+
const action_id = (0, uuid_1.v4)();
|
|
126
|
+
const input_payload = JSON.stringify(args);
|
|
127
|
+
if (name === 'write_file') {
|
|
128
|
+
const filePath = args.path;
|
|
129
|
+
const content = args.content;
|
|
130
|
+
const resolvedPath = path.resolve(filePath);
|
|
131
|
+
// Policy check before execution
|
|
132
|
+
const config = (0, engine_1.loadConfig)();
|
|
133
|
+
const policyResult = (0, engine_1.checkFileAction)(resolvedPath, 'write', config);
|
|
134
|
+
if (policyResult.decision === 'block') {
|
|
135
|
+
(0, database_1.insertAction)({
|
|
136
|
+
action_id, session_id: SESSION_ID, tool_name: 'write_file',
|
|
137
|
+
target_path: resolvedPath, input_payload, status: 'blocked',
|
|
138
|
+
decision: 'block', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
139
|
+
});
|
|
140
|
+
throw new Error(`Waymark blocked: ${policyResult.reason} [rule: ${policyResult.matchedRule}]`);
|
|
141
|
+
}
|
|
142
|
+
if (policyResult.decision === 'pending') {
|
|
143
|
+
(0, database_1.insertAction)({
|
|
144
|
+
action_id, session_id: SESSION_ID, tool_name: 'write_file',
|
|
145
|
+
target_path: resolvedPath, input_payload, status: 'pending',
|
|
146
|
+
decision: 'pending', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
147
|
+
});
|
|
148
|
+
// Fire-and-forget Slack notification (no console.log — MCP uses stdio)
|
|
149
|
+
(0, slack_1.notifyPendingAction)({
|
|
150
|
+
action_id, session_id: SESSION_ID, tool_name: 'write_file',
|
|
151
|
+
target_path: resolvedPath, input_payload,
|
|
152
|
+
before_snapshot: null, after_snapshot: null,
|
|
153
|
+
status: 'pending', error_message: null, stdout: null, stderr: null,
|
|
154
|
+
rolled_back: 0, rolled_back_at: null,
|
|
155
|
+
created_at: new Date().toISOString(),
|
|
156
|
+
decision: 'pending', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
157
|
+
approved_at: null, approved_by: null, rejected_at: null, rejected_reason: null,
|
|
158
|
+
id: 0,
|
|
159
|
+
}).catch(() => { });
|
|
160
|
+
return {
|
|
161
|
+
content: [{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: `Action requires approval.\nAction ID: ${action_id}\nCheck status: GET /api/actions/${action_id}/status\n\nThis action is pending human approval. You can continue with other tasks. Poll the status endpoint to check if approved. If approved, the action has already been executed. If rejected, check the status response for the reason.`,
|
|
164
|
+
}],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Capture before snapshot
|
|
168
|
+
let before_snapshot = null;
|
|
169
|
+
try {
|
|
170
|
+
before_snapshot = fs.readFileSync(resolvedPath, 'utf8');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// File doesn't exist yet — that's fine
|
|
174
|
+
}
|
|
175
|
+
(0, database_1.insertAction)({
|
|
176
|
+
action_id,
|
|
177
|
+
session_id: SESSION_ID,
|
|
178
|
+
tool_name: 'write_file',
|
|
179
|
+
target_path: resolvedPath,
|
|
180
|
+
input_payload,
|
|
181
|
+
before_snapshot,
|
|
182
|
+
status: 'pending',
|
|
183
|
+
});
|
|
184
|
+
try {
|
|
185
|
+
// Ensure directory exists
|
|
186
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
187
|
+
fs.writeFileSync(resolvedPath, content, 'utf8');
|
|
188
|
+
(0, database_1.updateAction)(action_id, {
|
|
189
|
+
status: 'success',
|
|
190
|
+
after_snapshot: content,
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text', text: `File written successfully: ${resolvedPath}` }],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
(0, database_1.updateAction)(action_id, {
|
|
198
|
+
status: 'error',
|
|
199
|
+
error_message: err.message,
|
|
200
|
+
});
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (name === 'read_file') {
|
|
205
|
+
const filePath = args.path;
|
|
206
|
+
const resolvedPath = path.resolve(filePath);
|
|
207
|
+
// Policy check before execution
|
|
208
|
+
const config = (0, engine_1.loadConfig)();
|
|
209
|
+
const policyResult = (0, engine_1.checkFileAction)(resolvedPath, 'read', config);
|
|
210
|
+
if (policyResult.decision === 'block') {
|
|
211
|
+
(0, database_1.insertAction)({
|
|
212
|
+
action_id, session_id: SESSION_ID, tool_name: 'read_file',
|
|
213
|
+
target_path: resolvedPath, input_payload, status: 'blocked',
|
|
214
|
+
decision: 'block', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
215
|
+
});
|
|
216
|
+
throw new Error(`Waymark blocked: ${policyResult.reason} [rule: ${policyResult.matchedRule}]`);
|
|
217
|
+
}
|
|
218
|
+
if (policyResult.decision === 'pending') {
|
|
219
|
+
(0, database_1.insertAction)({
|
|
220
|
+
action_id, session_id: SESSION_ID, tool_name: 'read_file',
|
|
221
|
+
target_path: resolvedPath, input_payload, status: 'pending',
|
|
222
|
+
decision: 'pending', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
223
|
+
});
|
|
224
|
+
(0, slack_1.notifyPendingAction)({
|
|
225
|
+
action_id, session_id: SESSION_ID, tool_name: 'read_file',
|
|
226
|
+
target_path: resolvedPath, input_payload,
|
|
227
|
+
before_snapshot: null, after_snapshot: null,
|
|
228
|
+
status: 'pending', error_message: null, stdout: null, stderr: null,
|
|
229
|
+
rolled_back: 0, rolled_back_at: null,
|
|
230
|
+
created_at: new Date().toISOString(),
|
|
231
|
+
decision: 'pending', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
232
|
+
approved_at: null, approved_by: null, rejected_at: null, rejected_reason: null,
|
|
233
|
+
id: 0,
|
|
234
|
+
}).catch(() => { });
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `Action requires approval.\nAction ID: ${action_id}\nCheck status: GET /api/actions/${action_id}/status\n\nThis action is pending human approval. You can continue with other tasks. Poll the status endpoint to check if approved. If approved, the action has already been executed. If rejected, check the status response for the reason.`,
|
|
239
|
+
}],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
(0, database_1.insertAction)({
|
|
243
|
+
action_id,
|
|
244
|
+
session_id: SESSION_ID,
|
|
245
|
+
tool_name: 'read_file',
|
|
246
|
+
target_path: resolvedPath,
|
|
247
|
+
input_payload,
|
|
248
|
+
status: 'pending',
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
252
|
+
(0, database_1.updateAction)(action_id, {
|
|
253
|
+
status: 'success',
|
|
254
|
+
after_snapshot: content,
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: 'text', text: content }],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
(0, database_1.updateAction)(action_id, {
|
|
262
|
+
status: 'error',
|
|
263
|
+
error_message: err.message,
|
|
264
|
+
});
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (name === 'bash') {
|
|
269
|
+
const command = args.command;
|
|
270
|
+
// Policy check before execution
|
|
271
|
+
const config = (0, engine_1.loadConfig)();
|
|
272
|
+
const policyResult = (0, engine_1.checkBashAction)(command, config);
|
|
273
|
+
if (policyResult.decision === 'block') {
|
|
274
|
+
(0, database_1.insertAction)({
|
|
275
|
+
action_id, session_id: SESSION_ID, tool_name: 'bash',
|
|
276
|
+
target_path: null, input_payload, status: 'blocked',
|
|
277
|
+
decision: 'block', policy_reason: policyResult.reason, matched_rule: policyResult.matchedRule,
|
|
278
|
+
});
|
|
279
|
+
throw new Error(`Waymark blocked command: ${policyResult.reason} [rule: ${policyResult.matchedRule}]`);
|
|
280
|
+
}
|
|
281
|
+
(0, database_1.insertAction)({
|
|
282
|
+
action_id,
|
|
283
|
+
session_id: SESSION_ID,
|
|
284
|
+
tool_name: 'bash',
|
|
285
|
+
target_path: null,
|
|
286
|
+
input_payload,
|
|
287
|
+
status: 'pending',
|
|
288
|
+
});
|
|
289
|
+
// Bug 1 + Bug 2: use spawnSync for clean stdout/stderr separation and USER_PATH
|
|
290
|
+
const result = (0, child_process_1.spawnSync)('sh', ['-c', command], {
|
|
291
|
+
encoding: 'utf8',
|
|
292
|
+
timeout: 30000,
|
|
293
|
+
env: { ...process.env, PATH: USER_PATH },
|
|
294
|
+
});
|
|
295
|
+
const stdout = result.stdout || '';
|
|
296
|
+
const stderr = result.stderr || '';
|
|
297
|
+
const failed = result.status !== 0 || !!result.error;
|
|
298
|
+
if (!failed) {
|
|
299
|
+
(0, database_1.updateAction)(action_id, {
|
|
300
|
+
status: 'success',
|
|
301
|
+
stdout,
|
|
302
|
+
stderr,
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: 'text', text: stdout || '(no output)' }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const errorMessage = result.error ? result.error.message : `Command exited with code ${result.status}`;
|
|
310
|
+
(0, database_1.updateAction)(action_id, {
|
|
311
|
+
status: 'error',
|
|
312
|
+
stdout,
|
|
313
|
+
stderr,
|
|
314
|
+
error_message: errorMessage,
|
|
315
|
+
});
|
|
316
|
+
throw new Error(stderr || errorMessage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
async function main() {
|
|
324
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
325
|
+
await server.connect(transport);
|
|
326
|
+
// MCP server communicates via stdio — no console.log here to avoid polluting the stream
|
|
327
|
+
}
|
|
328
|
+
main().catch((err) => {
|
|
329
|
+
process.stderr.write(`MCP server error: ${err.message}\n`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.notifyPendingAction = notifyPendingAction;
|
|
4
|
+
async function notifyPendingAction(action) {
|
|
5
|
+
const webhookUrl = process.env.WAYMARK_SLACK_WEBHOOK_URL;
|
|
6
|
+
if (!webhookUrl)
|
|
7
|
+
return;
|
|
8
|
+
const baseUrl = process.env.WAYMARK_BASE_URL || 'http://localhost:3001';
|
|
9
|
+
const dashboardUrl = `${baseUrl}/action/${action.action_id}`;
|
|
10
|
+
const timeAgo = (() => {
|
|
11
|
+
const diff = Math.floor((Date.now() - new Date(action.created_at + (action.created_at.includes('Z') ? '' : 'Z')).getTime()) / 1000);
|
|
12
|
+
if (diff < 60)
|
|
13
|
+
return `${diff}s ago`;
|
|
14
|
+
if (diff < 3600)
|
|
15
|
+
return `${Math.floor(diff / 60)}m ago`;
|
|
16
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
17
|
+
})();
|
|
18
|
+
const body = {
|
|
19
|
+
blocks: [
|
|
20
|
+
{
|
|
21
|
+
type: 'header',
|
|
22
|
+
text: { type: 'plain_text', text: '⏳ Waymark — Approval Required', emoji: true },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'section',
|
|
26
|
+
fields: [
|
|
27
|
+
{ type: 'mrkdwn', text: `*Tool:*\n${action.tool_name}` },
|
|
28
|
+
{ type: 'mrkdwn', text: `*Path:*\n${action.target_path || '—'}` },
|
|
29
|
+
{ type: 'mrkdwn', text: `*Rule:*\n${action.matched_rule || '—'}` },
|
|
30
|
+
{ type: 'mrkdwn', text: `*Time:*\n${timeAgo}` },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'actions',
|
|
35
|
+
elements: [
|
|
36
|
+
{
|
|
37
|
+
type: 'button',
|
|
38
|
+
text: { type: 'plain_text', text: 'View in Dashboard', emoji: true },
|
|
39
|
+
url: dashboardUrl,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'actions',
|
|
45
|
+
elements: [
|
|
46
|
+
{
|
|
47
|
+
type: 'button',
|
|
48
|
+
text: { type: 'plain_text', text: '✅ Approve', emoji: true },
|
|
49
|
+
style: 'primary',
|
|
50
|
+
action_id: 'waymark_approve',
|
|
51
|
+
value: action.action_id,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'button',
|
|
55
|
+
text: { type: 'plain_text', text: '❌ Reject', emoji: true },
|
|
56
|
+
style: 'danger',
|
|
57
|
+
action_id: 'waymark_reject',
|
|
58
|
+
value: action.action_id,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(webhookUrl, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
process.stderr.write(`Waymark Slack notification failed: ${res.status} ${res.statusText}\n`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
process.stderr.write(`Waymark Slack notification error: ${err.message}\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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.loadConfig = loadConfig;
|
|
40
|
+
exports.checkFileAction = checkFileAction;
|
|
41
|
+
exports.checkBashAction = checkBashAction;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const micromatch_1 = __importDefault(require("micromatch"));
|
|
45
|
+
const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
46
|
+
const CONFIG_PATH = path.join(PROJECT_ROOT, 'waymark.config.json');
|
|
47
|
+
const DEFAULT_CONFIG = {
|
|
48
|
+
version: '1',
|
|
49
|
+
policies: {
|
|
50
|
+
allowedPaths: [],
|
|
51
|
+
blockedPaths: [],
|
|
52
|
+
blockedCommands: [],
|
|
53
|
+
requireApproval: [],
|
|
54
|
+
maxBashOutputBytes: 10000,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
function loadConfig() {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
// Ensure all policy arrays exist
|
|
62
|
+
if (!parsed.policies)
|
|
63
|
+
return DEFAULT_CONFIG;
|
|
64
|
+
return {
|
|
65
|
+
version: parsed.version || '1',
|
|
66
|
+
policies: {
|
|
67
|
+
allowedPaths: parsed.policies.allowedPaths || [],
|
|
68
|
+
blockedPaths: parsed.policies.blockedPaths || [],
|
|
69
|
+
blockedCommands: parsed.policies.blockedCommands || [],
|
|
70
|
+
requireApproval: parsed.policies.requireApproval || [],
|
|
71
|
+
maxBashOutputBytes: parsed.policies.maxBashOutputBytes ?? 10000,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return DEFAULT_CONFIG;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolvePattern(pattern) {
|
|
80
|
+
// Absolute patterns pass through; relative (./...) resolve from project root
|
|
81
|
+
if (path.isAbsolute(pattern))
|
|
82
|
+
return pattern;
|
|
83
|
+
return path.resolve(PROJECT_ROOT, pattern);
|
|
84
|
+
}
|
|
85
|
+
function matchesAny(absFilePath, patterns) {
|
|
86
|
+
for (const pattern of patterns) {
|
|
87
|
+
const absPattern = resolvePattern(pattern);
|
|
88
|
+
if (micromatch_1.default.isMatch(absFilePath, absPattern, { dot: true })) {
|
|
89
|
+
return pattern;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
function checkFileAction(filePath, _action, config) {
|
|
95
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_ROOT, filePath);
|
|
96
|
+
const { blockedPaths, requireApproval, allowedPaths } = config.policies;
|
|
97
|
+
// 1. Blocked
|
|
98
|
+
const blockedMatch = matchesAny(absPath, blockedPaths);
|
|
99
|
+
if (blockedMatch) {
|
|
100
|
+
return {
|
|
101
|
+
decision: 'block',
|
|
102
|
+
reason: `Path matches blocked rule`,
|
|
103
|
+
matchedRule: blockedMatch,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// 2. Requires approval (queued/pending)
|
|
107
|
+
const approvalMatch = matchesAny(absPath, requireApproval);
|
|
108
|
+
if (approvalMatch) {
|
|
109
|
+
return {
|
|
110
|
+
decision: 'pending',
|
|
111
|
+
reason: `Path requires approval before execution`,
|
|
112
|
+
matchedRule: approvalMatch,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// 3. Allowed
|
|
116
|
+
const allowedMatch = matchesAny(absPath, allowedPaths);
|
|
117
|
+
if (allowedMatch) {
|
|
118
|
+
return {
|
|
119
|
+
decision: 'allow',
|
|
120
|
+
reason: `Path matches allowed rule`,
|
|
121
|
+
matchedRule: allowedMatch,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// 4. Default: block
|
|
125
|
+
return {
|
|
126
|
+
decision: 'block',
|
|
127
|
+
reason: `Path not in allowedPaths`,
|
|
128
|
+
matchedRule: '(default deny)',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function isCommandBlocked(command, rule) {
|
|
132
|
+
if (rule.startsWith('regex:')) {
|
|
133
|
+
const pattern = rule.slice(6);
|
|
134
|
+
try {
|
|
135
|
+
return new RegExp(pattern, 'i').test(command);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
console.warn(`Invalid regex in blockedCommands: ${rule}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return command.includes(rule);
|
|
143
|
+
}
|
|
144
|
+
function checkBashAction(command, config) {
|
|
145
|
+
const { blockedCommands } = config.policies;
|
|
146
|
+
for (const rule of blockedCommands) {
|
|
147
|
+
if (isCommandBlocked(command, rule)) {
|
|
148
|
+
const displayRule = rule.startsWith('regex:') ? rule.slice(6) : rule;
|
|
149
|
+
return {
|
|
150
|
+
decision: 'block',
|
|
151
|
+
reason: `Command matches blocked rule: "${displayRule}"`,
|
|
152
|
+
matchedRule: rule,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
decision: 'allow',
|
|
158
|
+
reason: 'Command allowed',
|
|
159
|
+
matchedRule: '(default allow)',
|
|
160
|
+
};
|
|
161
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shaifulshabuj-waymarks/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Waymark MCP server and dashboard",
|
|
5
|
+
"author": "Waymark <hello@waymarks.dev>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/waymarks/waymark",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/waymarks/waymark.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/waymarks/waymark/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ai-agents",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"mcp",
|
|
19
|
+
"security",
|
|
20
|
+
"developer-tools",
|
|
21
|
+
"llm",
|
|
22
|
+
"agent-control"
|
|
23
|
+
],
|
|
24
|
+
"main": "dist/api/server.js",
|
|
25
|
+
"bin": {
|
|
26
|
+
"waymark-server": "./dist/mcp/server.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist/**",
|
|
30
|
+
"src/ui/**"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"start": "node dist/api/server.js",
|
|
35
|
+
"dev:api": "nodemon --exec ts-node src/api/server.ts",
|
|
36
|
+
"dev:mcp": "ts-node src/mcp/server.ts",
|
|
37
|
+
"db:reset": "node -e \"require('fs').rmSync('./data/waymark.db', {force:true}); console.log('DB deleted.')\" && ts-node -e \"require('./src/db/database'); console.log('DB recreated.')\""
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
41
|
+
"better-sqlite3": "^9.4.3",
|
|
42
|
+
"dotenv": "^16.0.0",
|
|
43
|
+
"express": "^4.18.2",
|
|
44
|
+
"micromatch": "^4.0.5",
|
|
45
|
+
"uuid": "^9.0.0",
|
|
46
|
+
"concurrently": "^8.2.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
50
|
+
"@types/express": "^4.17.21",
|
|
51
|
+
"@types/micromatch": "^4.0.9",
|
|
52
|
+
"@types/node": "^20.11.5",
|
|
53
|
+
"@types/uuid": "^9.0.7",
|
|
54
|
+
"nodemon": "^3.0.3",
|
|
55
|
+
"ts-node": "^10.9.2",
|
|
56
|
+
"typescript": "^5.3.3"
|
|
57
|
+
}
|
|
58
|
+
}
|