causly-server 0.0.0-stage → 1.5.1
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/.env.example +120 -0
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +377 -3
- package/bin/causly.js +192 -0
- package/index.js +229 -0
- package/package.json +59 -6
- package/packs/calendar.js +141 -0
- package/packs/command.js +22 -0
- package/packs/database.js +95 -0
- package/packs/directory.js +63 -0
- package/packs/discord.js +63 -0
- package/packs/docker.js +202 -0
- package/packs/files.js +106 -0
- package/packs/git.js +211 -0
- package/packs/github.js +321 -0
- package/packs/gmail.js +98 -0
- package/packs/leads.js +46 -0
- package/packs/linear.js +104 -0
- package/packs/loader.js +247 -0
- package/packs/manifest.json +1152 -0
- package/packs/meetings.js +141 -0
- package/packs/notion.js +185 -0
- package/packs/pipeline.js +151 -0
- package/packs/project.js +70 -0
- package/packs/registry.js +149 -0
- package/packs/reminders.js +57 -0
- package/packs/research.js +34 -0
- package/packs/secrets.js +77 -0
- package/packs/sentry.js +97 -0
- package/packs/seo.js +70 -0
- package/packs/slack.js +95 -0
- package/packs/stripe.js +127 -0
- package/packs/supabase.js +73 -0
- package/packs/telegram.js +70 -0
- package/packs/terraform.js +247 -0
- package/packs/vercel.js +114 -0
- package/packs/workflow.js +84 -0
- package/prompts/cofounder.js +66 -0
- package/prompts/playbooks.js +75 -0
- package/tools/backupOps.js +87 -0
- package/tools/boundaryAttest.js +184 -0
- package/tools/calendarOps.js +563 -0
- package/tools/cliOps.js +233 -0
- package/tools/commandOps.js +198 -0
- package/tools/dbOps.js +117 -0
- package/tools/directoryOps.js +96 -0
- package/tools/discordOps.js +133 -0
- package/tools/dockerOps.js +259 -0
- package/tools/envFile.js +89 -0
- package/tools/envLoader.js +85 -0
- package/tools/fileOps.js +125 -0
- package/tools/gitOps.js +170 -0
- package/tools/githubOps.js +357 -0
- package/tools/gmailOps.js +221 -0
- package/tools/googleAuth.js +51 -0
- package/tools/googleLogin.js +230 -0
- package/tools/leadOps.js +447 -0
- package/tools/linearOps.js +304 -0
- package/tools/logger.js +47 -0
- package/tools/meetingOps.js +697 -0
- package/tools/notionOps.js +215 -0
- package/tools/paths.js +36 -0
- package/tools/pipelineOps.js +511 -0
- package/tools/projectOps.js +192 -0
- package/tools/reminderAlerts.js +48 -0
- package/tools/reminderOps.js +129 -0
- package/tools/scheduler.js +46 -0
- package/tools/secretsOps.js +133 -0
- package/tools/security.js +488 -0
- package/tools/sentryOps.js +130 -0
- package/tools/seoOps.js +561 -0
- package/tools/services.js +295 -0
- package/tools/setupOps.js +245 -0
- package/tools/slackOps.js +151 -0
- package/tools/statusOps.js +104 -0
- package/tools/store.js +111 -0
- package/tools/stripeOps.js +299 -0
- package/tools/supabaseOps.js +94 -0
- package/tools/telegramOps.js +153 -0
- package/tools/terminal.js +124 -0
- package/tools/terraformOps.js +171 -0
- package/tools/toolWrapper.js +60 -0
- package/tools/toolsetOps.js +123 -0
- package/tools/transcribeOps.js +221 -0
- package/tools/updateCheck.js +58 -0
- package/tools/vercelOps.js +198 -0
- package/tools/verifyBoundaryAttest.js +25 -0
- package/tools/webOps.js +474 -0
- package/tools/workflowOps.js +371 -0
- package/tools/workspace.js +85 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// Security subsystem: secret redaction, permission levels, and command
|
|
2
|
+
// risk classification. Single source of truth so every tool call, log
|
|
3
|
+
// line, and approval check uses the same rules.
|
|
4
|
+
|
|
5
|
+
// ---------------- Secret redaction ----------------
|
|
6
|
+
|
|
7
|
+
// Key names (case-insensitive substring match) whose values must never
|
|
8
|
+
// appear in logs, error messages, or tool responses.
|
|
9
|
+
const SECRET_KEY_PATTERNS = [
|
|
10
|
+
"token",
|
|
11
|
+
"password",
|
|
12
|
+
"passwd",
|
|
13
|
+
"api_key",
|
|
14
|
+
"apikey",
|
|
15
|
+
"secret",
|
|
16
|
+
"authorization",
|
|
17
|
+
"access_token",
|
|
18
|
+
"refresh_token",
|
|
19
|
+
"db_pass",
|
|
20
|
+
"credential",
|
|
21
|
+
"private_key",
|
|
22
|
+
"client_secret",
|
|
23
|
+
"auth_token",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const REDACTED = "[REDACTED]";
|
|
27
|
+
|
|
28
|
+
function isSecretKey(key) {
|
|
29
|
+
const lower = key.toLowerCase();
|
|
30
|
+
return SECRET_KEY_PATTERNS.some((pattern) => lower.includes(pattern));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recursively walk an object/array and replace the value of any key that
|
|
35
|
+
* looks like a secret with "[REDACTED]". Never mutates the input — returns
|
|
36
|
+
* a deep copy. Safe against circular references (bails out to "[Circular]").
|
|
37
|
+
*/
|
|
38
|
+
export function redactSecrets(value, seen = new WeakSet()) {
|
|
39
|
+
if (value === null || value === undefined) return value;
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
if (seen.has(value)) return "[Circular]";
|
|
43
|
+
seen.add(value);
|
|
44
|
+
return value.map((item) => redactSecrets(item, seen));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === "object") {
|
|
48
|
+
if (seen.has(value)) return "[Circular]";
|
|
49
|
+
seen.add(value);
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, val] of Object.entries(value)) {
|
|
52
|
+
out[key] = isSecretKey(key) ? REDACTED : redactSecrets(val, seen);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Redact secret-looking substrings out of a plain string (e.g. an error
|
|
62
|
+
* message that happens to include "token=abc123..."). Best-effort — key/
|
|
63
|
+
* value pairs like `token=xxx`, `token: xxx`, `"token":"xxx"` are caught;
|
|
64
|
+
* this is not a substitute for redactSecrets() on structured data.
|
|
65
|
+
*/
|
|
66
|
+
export function redactSecretsInString(str) {
|
|
67
|
+
if (typeof str !== "string") return str;
|
|
68
|
+
const pattern = new RegExp(
|
|
69
|
+
`\\b(${SECRET_KEY_PATTERNS.join("|")})\\b(\\s*[:=]\\s*)("[^"]*"|'[^']*'|\\S+)`,
|
|
70
|
+
"gi"
|
|
71
|
+
);
|
|
72
|
+
return str.replace(pattern, (_match, key, sep) => `${key}${sep}${REDACTED}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------- Permission levels ----------------
|
|
76
|
+
|
|
77
|
+
// READ — safe, read-only, no side effects
|
|
78
|
+
// LOW — normal dev actions, low blast radius, easily reversible
|
|
79
|
+
// MEDIUM — modifies project/repo state, reversible with effort
|
|
80
|
+
// HIGH — production-impacting or hard to reverse (pushes, deploys,
|
|
81
|
+
// merges, arbitrary shell/SQL execution)
|
|
82
|
+
// DESTRUCTIVE — deletion / irreversible
|
|
83
|
+
//
|
|
84
|
+
// Approval policy: READ and LOW run automatically. MEDIUM runs
|
|
85
|
+
// automatically too in this single-user trusted-local-machine mode, but
|
|
86
|
+
// is classified so that policy can be tightened later without touching
|
|
87
|
+
// every tool. HIGH and DESTRUCTIVE always require the caller to pass
|
|
88
|
+
// `confirm: true` — this is the approval gate.
|
|
89
|
+
export const PERMISSION_LEVELS = {
|
|
90
|
+
// Files
|
|
91
|
+
read_file: "READ",
|
|
92
|
+
read_multiple_files: "READ",
|
|
93
|
+
create_file: "MEDIUM",
|
|
94
|
+
write_file: "MEDIUM",
|
|
95
|
+
edit_file: "MEDIUM",
|
|
96
|
+
delete_file: "DESTRUCTIVE",
|
|
97
|
+
move_file: "MEDIUM",
|
|
98
|
+
copy_file: "LOW",
|
|
99
|
+
get_file_info: "READ",
|
|
100
|
+
|
|
101
|
+
// Directory
|
|
102
|
+
list_directory: "READ",
|
|
103
|
+
directory_tree: "READ",
|
|
104
|
+
create_directory: "LOW",
|
|
105
|
+
delete_directory: "DESTRUCTIVE",
|
|
106
|
+
search_files: "READ",
|
|
107
|
+
|
|
108
|
+
// Git
|
|
109
|
+
git_init: "LOW",
|
|
110
|
+
git_status: "READ",
|
|
111
|
+
git_add: "LOW",
|
|
112
|
+
git_commit: "MEDIUM",
|
|
113
|
+
git_push: "MEDIUM",
|
|
114
|
+
git_pull: "MEDIUM",
|
|
115
|
+
git_log: "READ",
|
|
116
|
+
git_diff: "READ",
|
|
117
|
+
git_branch: "READ",
|
|
118
|
+
git_create_branch: "LOW",
|
|
119
|
+
git_checkout: "MEDIUM",
|
|
120
|
+
git_merge: "MEDIUM",
|
|
121
|
+
git_reset: "HIGH",
|
|
122
|
+
git_stash: "LOW",
|
|
123
|
+
git_show: "READ",
|
|
124
|
+
git_remote: "MEDIUM",
|
|
125
|
+
git_tag: "LOW",
|
|
126
|
+
git_changed_files: "READ",
|
|
127
|
+
git_diff_stat: "READ",
|
|
128
|
+
git_check_clean: "READ",
|
|
129
|
+
|
|
130
|
+
// Command
|
|
131
|
+
run_command: "HIGH",
|
|
132
|
+
|
|
133
|
+
// GitHub
|
|
134
|
+
github_get_authenticated_user: "READ",
|
|
135
|
+
github_create_repo: "MEDIUM",
|
|
136
|
+
github_delete_repo: "DESTRUCTIVE",
|
|
137
|
+
github_list_repos: "READ",
|
|
138
|
+
github_create_issue: "LOW",
|
|
139
|
+
github_list_issues: "READ",
|
|
140
|
+
github_create_pull_request: "MEDIUM",
|
|
141
|
+
github_list_pull_requests: "READ",
|
|
142
|
+
github_add_comment: "LOW",
|
|
143
|
+
github_get_repo: "READ",
|
|
144
|
+
github_get_issue: "READ",
|
|
145
|
+
github_update_issue: "MEDIUM",
|
|
146
|
+
github_get_pull_request: "READ",
|
|
147
|
+
github_update_pull_request: "MEDIUM",
|
|
148
|
+
github_merge_pull_request: "HIGH",
|
|
149
|
+
github_get_pull_request_files: "READ",
|
|
150
|
+
github_get_pull_request_diff: "READ",
|
|
151
|
+
github_get_pull_request_comments: "READ",
|
|
152
|
+
github_get_pull_request_reviews: "READ",
|
|
153
|
+
github_get_branch: "READ",
|
|
154
|
+
github_list_branches: "READ",
|
|
155
|
+
github_list_workflows: "READ",
|
|
156
|
+
github_list_workflow_runs: "READ",
|
|
157
|
+
github_get_workflow_run: "READ",
|
|
158
|
+
github_get_workflow_run_jobs: "READ",
|
|
159
|
+
github_get_job_logs: "READ",
|
|
160
|
+
github_rerun_workflow: "MEDIUM",
|
|
161
|
+
|
|
162
|
+
// Vercel
|
|
163
|
+
vercel_get_authenticated_user: "READ",
|
|
164
|
+
vercel_list_projects: "READ",
|
|
165
|
+
vercel_get_project: "READ",
|
|
166
|
+
vercel_list_deployments: "READ",
|
|
167
|
+
vercel_get_deployment: "READ",
|
|
168
|
+
vercel_create_deployment: "HIGH",
|
|
169
|
+
vercel_delete_project: "DESTRUCTIVE",
|
|
170
|
+
vercel_get_deployment_logs: "READ",
|
|
171
|
+
vercel_get_deployment_events: "READ",
|
|
172
|
+
vercel_cancel_deployment: "MEDIUM",
|
|
173
|
+
http_check: "READ",
|
|
174
|
+
|
|
175
|
+
// Project intelligence
|
|
176
|
+
project_detect: "READ",
|
|
177
|
+
project_info: "READ",
|
|
178
|
+
project_health: "READ",
|
|
179
|
+
run_tests: "LOW",
|
|
180
|
+
run_lint: "LOW",
|
|
181
|
+
run_typecheck: "LOW",
|
|
182
|
+
run_build: "LOW",
|
|
183
|
+
|
|
184
|
+
// Workflows
|
|
185
|
+
ship_change: "HIGH",
|
|
186
|
+
fix_ci: "READ",
|
|
187
|
+
verify_ci_fix: "HIGH",
|
|
188
|
+
deploy_project: "HIGH",
|
|
189
|
+
|
|
190
|
+
// Slack
|
|
191
|
+
slack_get_user: "READ",
|
|
192
|
+
slack_list_channels: "READ",
|
|
193
|
+
slack_get_channel: "READ",
|
|
194
|
+
slack_read_messages: "READ",
|
|
195
|
+
slack_search_messages: "READ",
|
|
196
|
+
slack_send_message: "HIGH",
|
|
197
|
+
slack_reply_thread: "HIGH",
|
|
198
|
+
slack_create_channel: "HIGH",
|
|
199
|
+
|
|
200
|
+
// Gmail
|
|
201
|
+
gmail_get_profile: "READ",
|
|
202
|
+
gmail_search: "READ",
|
|
203
|
+
gmail_list_messages: "READ",
|
|
204
|
+
gmail_get_message: "READ",
|
|
205
|
+
gmail_get_thread: "READ",
|
|
206
|
+
gmail_send: "HIGH",
|
|
207
|
+
gmail_reply: "HIGH",
|
|
208
|
+
gmail_forward: "HIGH",
|
|
209
|
+
|
|
210
|
+
// Supabase
|
|
211
|
+
supabase_list_organizations: "READ",
|
|
212
|
+
supabase_list_projects: "READ",
|
|
213
|
+
supabase_get_project: "READ",
|
|
214
|
+
supabase_create_project: "MEDIUM",
|
|
215
|
+
supabase_delete_project: "DESTRUCTIVE",
|
|
216
|
+
supabase_run_sql: "HIGH",
|
|
217
|
+
|
|
218
|
+
// Database
|
|
219
|
+
postgres_query: "HIGH",
|
|
220
|
+
postgres_list_tables: "READ",
|
|
221
|
+
postgres_describe_table: "READ",
|
|
222
|
+
postgres_test_connection: "READ",
|
|
223
|
+
mysql_query: "HIGH",
|
|
224
|
+
mysql_list_tables: "READ",
|
|
225
|
+
mysql_describe_table: "READ",
|
|
226
|
+
mysql_test_connection: "READ",
|
|
227
|
+
|
|
228
|
+
// Secrets
|
|
229
|
+
secrets_set: "HIGH",
|
|
230
|
+
secrets_get: "MEDIUM",
|
|
231
|
+
secrets_list: "READ",
|
|
232
|
+
secrets_delete: "DESTRUCTIVE",
|
|
233
|
+
secrets_rotate_key: "HIGH",
|
|
234
|
+
secrets_generate_key: "LOW",
|
|
235
|
+
|
|
236
|
+
// Sentry
|
|
237
|
+
sentry_list_projects: "READ",
|
|
238
|
+
sentry_list_issues: "READ",
|
|
239
|
+
sentry_search_issues: "READ",
|
|
240
|
+
sentry_get_issue: "READ",
|
|
241
|
+
sentry_get_project_stats: "READ",
|
|
242
|
+
sentry_resolve_issue: "HIGH",
|
|
243
|
+
sentry_ignore_issue: "HIGH",
|
|
244
|
+
sentry_add_comment: "HIGH",
|
|
245
|
+
|
|
246
|
+
// Notion
|
|
247
|
+
notion_search: "READ",
|
|
248
|
+
notion_get_page: "READ",
|
|
249
|
+
notion_get_database: "READ",
|
|
250
|
+
notion_query_database: "READ",
|
|
251
|
+
notion_get_block_children: "READ",
|
|
252
|
+
notion_get_comments: "READ",
|
|
253
|
+
notion_list_users: "READ",
|
|
254
|
+
notion_get_user: "READ",
|
|
255
|
+
notion_create_page: "HIGH",
|
|
256
|
+
notion_update_page: "HIGH",
|
|
257
|
+
notion_create_database: "HIGH",
|
|
258
|
+
notion_append_block_children: "HIGH",
|
|
259
|
+
notion_update_block: "HIGH",
|
|
260
|
+
notion_add_comment: "HIGH",
|
|
261
|
+
notion_delete_block: "HIGH",
|
|
262
|
+
|
|
263
|
+
// Terraform
|
|
264
|
+
terraform_init: "LOW",
|
|
265
|
+
terraform_validate: "READ",
|
|
266
|
+
terraform_fmt: "MEDIUM",
|
|
267
|
+
terraform_plan: "LOW",
|
|
268
|
+
terraform_apply: "HIGH",
|
|
269
|
+
terraform_destroy: "DESTRUCTIVE",
|
|
270
|
+
terraform_show: "READ",
|
|
271
|
+
terraform_output: "READ",
|
|
272
|
+
terraform_workspace: "HIGH",
|
|
273
|
+
terraform_state_list: "READ",
|
|
274
|
+
terraform_state_show: "READ",
|
|
275
|
+
terraform_state_pull: "READ",
|
|
276
|
+
terraform_state_mv: "HIGH",
|
|
277
|
+
terraform_state_rm: "HIGH",
|
|
278
|
+
terraform_import: "HIGH",
|
|
279
|
+
terraform_taint: "HIGH",
|
|
280
|
+
terraform_untaint: "MEDIUM",
|
|
281
|
+
terraform_graph: "READ",
|
|
282
|
+
terraform_providers: "READ",
|
|
283
|
+
terraform_plan_comment: "MEDIUM",
|
|
284
|
+
|
|
285
|
+
// Docker
|
|
286
|
+
docker_version: "READ",
|
|
287
|
+
docker_ps: "READ",
|
|
288
|
+
docker_images: "READ",
|
|
289
|
+
docker_logs: "READ",
|
|
290
|
+
docker_inspect: "READ",
|
|
291
|
+
docker_stats: "READ",
|
|
292
|
+
docker_start: "LOW",
|
|
293
|
+
docker_pull: "MEDIUM",
|
|
294
|
+
docker_build: "HIGH",
|
|
295
|
+
docker_run: "HIGH",
|
|
296
|
+
docker_stop: "HIGH",
|
|
297
|
+
docker_restart: "HIGH",
|
|
298
|
+
docker_exec: "HIGH",
|
|
299
|
+
docker_push: "HIGH",
|
|
300
|
+
docker_compose_up: "HIGH",
|
|
301
|
+
docker_compose_down: "HIGH",
|
|
302
|
+
docker_remove: "DESTRUCTIVE",
|
|
303
|
+
docker_remove_image: "DESTRUCTIVE",
|
|
304
|
+
|
|
305
|
+
// Toolset management
|
|
306
|
+
search_tools: "READ",
|
|
307
|
+
load_toolset: "LOW",
|
|
308
|
+
use_tool: "READ", // only forwards; the called tool applies its own risk level and approvals
|
|
309
|
+
|
|
310
|
+
// Reminders
|
|
311
|
+
reminder_create: "LOW",
|
|
312
|
+
reminder_list: "READ",
|
|
313
|
+
reminder_update: "LOW",
|
|
314
|
+
|
|
315
|
+
// Research
|
|
316
|
+
web_search: "READ",
|
|
317
|
+
web_fetch: "MEDIUM",
|
|
318
|
+
|
|
319
|
+
// Leads
|
|
320
|
+
lead_hn_hiring: "READ",
|
|
321
|
+
lead_remote_jobs: "READ",
|
|
322
|
+
lead_company_contacts: "MEDIUM",
|
|
323
|
+
|
|
324
|
+
// SEO and content
|
|
325
|
+
seo_keyword_ideas: "READ",
|
|
326
|
+
seo_page_audit: "MEDIUM",
|
|
327
|
+
seo_content_check: "READ",
|
|
328
|
+
seo_search_console: "READ",
|
|
329
|
+
|
|
330
|
+
// Pipeline
|
|
331
|
+
pipeline_add_lead: "LOW",
|
|
332
|
+
pipeline_list: "READ",
|
|
333
|
+
pipeline_get: "READ",
|
|
334
|
+
pipeline_update: "LOW",
|
|
335
|
+
pipeline_log_activity: "LOW",
|
|
336
|
+
pipeline_add_contact: "LOW",
|
|
337
|
+
pipeline_report: "READ",
|
|
338
|
+
pipeline_export: "MEDIUM",
|
|
339
|
+
pipeline_delete_deal: "DESTRUCTIVE",
|
|
340
|
+
|
|
341
|
+
// Google Calendar (inviting guests is gated separately inside the tools)
|
|
342
|
+
calendar_list_calendars: "READ",
|
|
343
|
+
calendar_list_events: "READ",
|
|
344
|
+
calendar_get_event: "READ",
|
|
345
|
+
calendar_free_busy: "READ",
|
|
346
|
+
calendar_find_slots: "READ",
|
|
347
|
+
calendar_create_event: "MEDIUM",
|
|
348
|
+
calendar_update_event: "MEDIUM",
|
|
349
|
+
calendar_delete_event: "DESTRUCTIVE",
|
|
350
|
+
|
|
351
|
+
// Meeting notes (transcribing uploads audio to Groq, so it needs confirm)
|
|
352
|
+
meeting_transcribe: "HIGH",
|
|
353
|
+
meeting_save: "MEDIUM",
|
|
354
|
+
meeting_list: "READ",
|
|
355
|
+
meeting_get: "READ",
|
|
356
|
+
meeting_search: "READ",
|
|
357
|
+
meeting_actions: "READ",
|
|
358
|
+
meeting_complete_action: "LOW",
|
|
359
|
+
meeting_prep: "READ",
|
|
360
|
+
meeting_delete: "DESTRUCTIVE",
|
|
361
|
+
|
|
362
|
+
// Telegram (notify only reaches the owner's own chat)
|
|
363
|
+
telegram_get_me: "READ",
|
|
364
|
+
telegram_get_updates: "READ",
|
|
365
|
+
telegram_get_chat: "READ",
|
|
366
|
+
telegram_notify: "LOW",
|
|
367
|
+
telegram_send_message: "HIGH",
|
|
368
|
+
|
|
369
|
+
// Linear
|
|
370
|
+
linear_get_viewer: "READ",
|
|
371
|
+
linear_list_teams: "READ",
|
|
372
|
+
linear_list_issues: "READ",
|
|
373
|
+
linear_get_issue: "READ",
|
|
374
|
+
linear_create_issue: "MEDIUM",
|
|
375
|
+
linear_update_issue: "MEDIUM",
|
|
376
|
+
linear_add_comment: "MEDIUM",
|
|
377
|
+
|
|
378
|
+
// Stripe (live keys are refused unless STRIPE_ALLOW_LIVE=true; anything that emails or moves money is HIGH)
|
|
379
|
+
stripe_get_account: "READ",
|
|
380
|
+
stripe_list_customers: "READ",
|
|
381
|
+
stripe_create_customer: "MEDIUM",
|
|
382
|
+
stripe_list_invoices: "READ",
|
|
383
|
+
stripe_create_invoice: "MEDIUM",
|
|
384
|
+
stripe_send_invoice: "HIGH",
|
|
385
|
+
stripe_create_payment_link: "HIGH",
|
|
386
|
+
stripe_list_payments: "READ",
|
|
387
|
+
stripe_refund: "HIGH",
|
|
388
|
+
|
|
389
|
+
// Discord
|
|
390
|
+
discord_get_me: "READ",
|
|
391
|
+
discord_list_guilds: "READ",
|
|
392
|
+
discord_list_channels: "READ",
|
|
393
|
+
discord_read_messages: "READ",
|
|
394
|
+
discord_send_message: "HIGH",
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const APPROVAL_REQUIRED_LEVELS = new Set(["HIGH", "DESTRUCTIVE"]);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Checks whether a tool call is allowed to proceed. HIGH and DESTRUCTIVE
|
|
401
|
+
* tools require the caller to explicitly pass `confirm: true` in their
|
|
402
|
+
* input — this is the approval gate for this single-user trusted-machine
|
|
403
|
+
* server (no separate human-approval UI exists at the MCP layer, so the
|
|
404
|
+
* explicit flag stands in for it: the AI must consciously decide to
|
|
405
|
+
* proceed, typically after confirming with the person).
|
|
406
|
+
*
|
|
407
|
+
* Returns { allowed, level, reason }. Never throws.
|
|
408
|
+
*/
|
|
409
|
+
export function checkApproval(toolName, input) {
|
|
410
|
+
const level = PERMISSION_LEVELS[toolName] || "MEDIUM";
|
|
411
|
+
if (!APPROVAL_REQUIRED_LEVELS.has(level)) {
|
|
412
|
+
return { allowed: true, level };
|
|
413
|
+
}
|
|
414
|
+
if (input && input.confirm === true) {
|
|
415
|
+
return { allowed: true, level };
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
allowed: false,
|
|
419
|
+
level,
|
|
420
|
+
reason: `'${toolName}' is a ${level}-risk action and requires explicit confirmation. Re-call with confirm: true to proceed.`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------- Command risk classification ----------------
|
|
425
|
+
|
|
426
|
+
// Beyond the hard BLOCKED_PATTERNS in commandOps.js (which stop a call
|
|
427
|
+
// outright), this gives a *graded* risk read on a command string so it can
|
|
428
|
+
// be surfaced in logs/results for auditability — per the roadmap's own
|
|
429
|
+
// warning: "Do not rely only on regex-based destructive command blocking."
|
|
430
|
+
const RISKY_COMMAND_SIGNALS = [
|
|
431
|
+
{ pattern: /\b(rm|del|erase)\b/i, reason: "deletes files" },
|
|
432
|
+
{ pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "drops a database object" },
|
|
433
|
+
{ pattern: /\btruncate\s+table\b/i, reason: "truncates a table" },
|
|
434
|
+
{ pattern: /\bgit\s+push\s+.*--force/i, reason: "force-pushes (can overwrite remote history)" },
|
|
435
|
+
{ pattern: /\bgit\s+reset\s+--hard/i, reason: "hard-resets (can discard uncommitted work)" },
|
|
436
|
+
{ pattern: /\bcurl\b.*\|\s*(bash|sh|powershell)/i, reason: "pipes a remote script directly into a shell" },
|
|
437
|
+
{ pattern: /\biwr\b.*\|\s*iex\b/i, reason: "downloads and executes a remote script (PowerShell)" },
|
|
438
|
+
{ pattern: /\bchmod\s+-R\s+777\b/i, reason: "recursively opens permissions" },
|
|
439
|
+
{ pattern: /\bsudo\b|\brunas\b/i, reason: "elevates privileges" },
|
|
440
|
+
{ pattern: />\s*\/dev\/(sd|nvme|hd)/i, reason: "writes directly to a disk device" },
|
|
441
|
+
{ pattern: /\bnpm\s+publish\b/i, reason: "publishes a package publicly" },
|
|
442
|
+
{ pattern: /\bgh\s+repo\s+delete\b/i, reason: "deletes a GitHub repository" },
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Returns a risk read on a shell command string: { risk: "LOW"|"ELEVATED",
|
|
447
|
+
* signals: [reasons] }. Purely informational (does not block) — the hard
|
|
448
|
+
* BLOCKED_PATTERNS check in commandOps.js still runs separately and does
|
|
449
|
+
* block outright. This exists so risky-but-legitimate commands (force
|
|
450
|
+
* push, drop table, etc.) are visible in the audit log even when allowed.
|
|
451
|
+
*/
|
|
452
|
+
export function classifyCommandRisk(command) {
|
|
453
|
+
const signals = RISKY_COMMAND_SIGNALS.filter((s) => s.pattern.test(command)).map((s) => s.reason);
|
|
454
|
+
return { risk: signals.length ? "ELEVATED" : "LOW", signals };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---------------- Path security ----------------
|
|
458
|
+
|
|
459
|
+
// System-critical directories that must never be targeted by delete/write
|
|
460
|
+
// operations, even on a trusted single-user machine — a wrong path here
|
|
461
|
+
// is the difference between "oops, undo" and "reinstall Windows". Matched
|
|
462
|
+
// case-insensitively against the resolved absolute path prefix.
|
|
463
|
+
const DENIED_PATH_PREFIXES = [
|
|
464
|
+
"C:\\Windows",
|
|
465
|
+
"C:\\Program Files",
|
|
466
|
+
"C:\\Program Files (x86)",
|
|
467
|
+
"C:\\ProgramData",
|
|
468
|
+
"C:\\Users\\Default",
|
|
469
|
+
"C:\\Users\\Public",
|
|
470
|
+
"C:\\System Volume Information",
|
|
471
|
+
"C:\\Recovery",
|
|
472
|
+
"C:\\Boot",
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Checks whether a path falls inside a denied system directory. Compares
|
|
477
|
+
* the normalized, lowercased path against known-dangerous prefixes.
|
|
478
|
+
* Returns { denied, reason }.
|
|
479
|
+
*/
|
|
480
|
+
export function isPathDenied(targetPath) {
|
|
481
|
+
if (!targetPath || typeof targetPath !== "string") return { denied: false };
|
|
482
|
+
const normalized = targetPath.replace(/\//g, "\\").toLowerCase();
|
|
483
|
+
const hit = DENIED_PATH_PREFIXES.find((prefix) => normalized.startsWith(prefix.toLowerCase()));
|
|
484
|
+
if (hit) {
|
|
485
|
+
return { denied: true, reason: `'${targetPath}' is inside a protected system directory (${hit}) and cannot be modified through this tool.` };
|
|
486
|
+
}
|
|
487
|
+
return { denied: false };
|
|
488
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const SENTRY_HOSTS = ["https://sentry.io/api/0", "https://de.sentry.io/api/0"];
|
|
2
|
+
|
|
3
|
+
function getToken() {
|
|
4
|
+
const token = process.env.SENTRY_AUTH_TOKEN;
|
|
5
|
+
if (!token) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"SENTRY_AUTH_TOKEN not set. Create one at https://sentry.io/settings/account/api/auth-tokens/ and add it to your .env file: SENTRY_AUTH_TOKEN=..."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return token;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function sentryFetchOnHost(host, path, { method = "GET", body } = {}) {
|
|
14
|
+
const token = getToken();
|
|
15
|
+
const res = await fetch(`${host}${path}`, {
|
|
16
|
+
method,
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
},
|
|
21
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
});
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
const data = text ? JSON.parse(text) : {};
|
|
25
|
+
return { ok: res.ok, status: res.status, data };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Some Sentry endpoints (notably bare /issues/{id}/ lookups) only resolve on
|
|
30
|
+
* the organization's actual data-residency host (e.g. de.sentry.io for EU
|
|
31
|
+
* orgs), even though org/project-scoped endpoints work fine on sentry.io.
|
|
32
|
+
* We try the default host first, and on a 404 transparently retry on the
|
|
33
|
+
* other known region host before giving up.
|
|
34
|
+
*/
|
|
35
|
+
async function sentryFetch(path, opts = {}) {
|
|
36
|
+
let last;
|
|
37
|
+
for (const host of SENTRY_HOSTS) {
|
|
38
|
+
const result = await sentryFetchOnHost(host, path, opts);
|
|
39
|
+
if (result.ok) return result.data;
|
|
40
|
+
last = result;
|
|
41
|
+
if (result.status !== 404) break;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Sentry API error (${path}): ${last.data.detail || last.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function simplifyIssue(i) {
|
|
47
|
+
return {
|
|
48
|
+
id: i.id,
|
|
49
|
+
shortId: i.shortId,
|
|
50
|
+
title: i.title,
|
|
51
|
+
culprit: i.culprit,
|
|
52
|
+
level: i.level,
|
|
53
|
+
status: i.status,
|
|
54
|
+
count: i.count,
|
|
55
|
+
userCount: i.userCount,
|
|
56
|
+
firstSeen: i.firstSeen,
|
|
57
|
+
lastSeen: i.lastSeen,
|
|
58
|
+
permalink: i.permalink,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** List projects in the organization. */
|
|
63
|
+
export async function sentryListProjects({ org_slug }) {
|
|
64
|
+
const data = await sentryFetch(`/organizations/${org_slug}/projects/`);
|
|
65
|
+
return {
|
|
66
|
+
projects: data.map((p) => ({ id: p.id, slug: p.slug, name: p.name, platform: p.platform })),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** List recent issues (errors) for a project. */
|
|
71
|
+
export async function sentryListIssues({ org_slug, project_slug, query, limit = 25 }) {
|
|
72
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
73
|
+
if (query) params.set("query", query);
|
|
74
|
+
const data = await sentryFetch(`/projects/${org_slug}/${project_slug}/issues/?${params}`);
|
|
75
|
+
return { issues: data.map(simplifyIssue) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Search issues with a Sentry query string (e.g. "is:unresolved level:error"). */
|
|
79
|
+
export async function sentrySearchIssues({ org_slug, project_slug, query, limit = 25 }) {
|
|
80
|
+
return sentryListIssues({ org_slug, project_slug, query, limit });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Get a single issue's full detail (stack trace context, occurrence counts). */
|
|
84
|
+
export async function sentryGetIssue({ issue_id }) {
|
|
85
|
+
const data = await sentryFetch(`/issues/${issue_id}/`);
|
|
86
|
+
return {
|
|
87
|
+
...simplifyIssue(data),
|
|
88
|
+
metadata: data.metadata,
|
|
89
|
+
type: data.type,
|
|
90
|
+
platform: data.platform,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Mark an issue as resolved. HIGH risk — requires confirm: true. */
|
|
95
|
+
export async function sentryResolveIssue({ issue_id, confirm }) {
|
|
96
|
+
const data = await sentryFetch(`/issues/${issue_id}/`, {
|
|
97
|
+
method: "PUT",
|
|
98
|
+
body: { status: "resolved" },
|
|
99
|
+
});
|
|
100
|
+
return { id: data.id, status: data.status };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Mute/ignore an issue so it stops notifying. HIGH risk — requires confirm: true. */
|
|
104
|
+
export async function sentryIgnoreIssue({ issue_id, confirm }) {
|
|
105
|
+
const data = await sentryFetch(`/issues/${issue_id}/`, {
|
|
106
|
+
method: "PUT",
|
|
107
|
+
body: { status: "ignored" },
|
|
108
|
+
});
|
|
109
|
+
return { id: data.id, status: data.status };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Get error-count stats/trends for a project over a time period. */
|
|
113
|
+
export async function sentryGetProjectStats({ org_slug, project_slug, stat = "received", period = "24h" }) {
|
|
114
|
+
const params = new URLSearchParams({ stat, resolution: "1h" });
|
|
115
|
+
const data = await sentryFetch(`/projects/${org_slug}/${project_slug}/stats/?${params}`);
|
|
116
|
+
return {
|
|
117
|
+
period,
|
|
118
|
+
stat,
|
|
119
|
+
points: data.map(([timestamp, value]) => ({ timestamp, value })),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Add a comment/note to an issue. HIGH risk — requires confirm: true. */
|
|
124
|
+
export async function sentryAddComment({ issue_id, text, confirm }) {
|
|
125
|
+
const data = await sentryFetch(`/issues/${issue_id}/comments/`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
body: { text },
|
|
128
|
+
});
|
|
129
|
+
return { id: data.id, posted: true };
|
|
130
|
+
}
|