@bahulam/code 2.6.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/README.md +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk tier classifier — Mission Control (PRD-055 §8.1).
|
|
3
|
+
*
|
|
4
|
+
* import { classify, TIERS, behavior } from './risk-tier.mjs';
|
|
5
|
+
* const tier = classify('shell', { command: 'rm -rf node_modules' });
|
|
6
|
+
* // → 'shell-dangerous'
|
|
7
|
+
*
|
|
8
|
+
* Pure: no I/O, no async. Tested in isolation; the rest of the CLI relies on
|
|
9
|
+
* the return value to decide whether to auto-approve, auto-approve with
|
|
10
|
+
* checkpoint, or hold for explicit approval.
|
|
11
|
+
*
|
|
12
|
+
* The CLI never asks the backend for the tier. The backend's job is to say
|
|
13
|
+
* "this is what I want to run"; we map that to a tier locally so dangerous
|
|
14
|
+
* intent can't be hidden behind a friendly description.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ── Tier enum ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const TIERS = Object.freeze({
|
|
20
|
+
READ: 'read',
|
|
21
|
+
SENSITIVE_READ: 'sensitive-read',
|
|
22
|
+
LOCAL_EDIT: 'local-edit',
|
|
23
|
+
SHELL_SAFE: 'shell-safe',
|
|
24
|
+
SHELL_MEDIUM: 'shell-medium',
|
|
25
|
+
SHELL_DANGEROUS: 'shell-dangerous',
|
|
26
|
+
DESTRUCTIVE: 'destructive',
|
|
27
|
+
NETWORK: 'network',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Default behavior by tier:
|
|
32
|
+
* auto — proceed silently
|
|
33
|
+
* auto-with-undo — proceed but record a checkpoint first
|
|
34
|
+
* prompt-safe — prompt with Enter=approve default
|
|
35
|
+
* prompt-explicit — magenta-bordered prompt, no default
|
|
36
|
+
*/
|
|
37
|
+
export const BEHAVIOR = Object.freeze({
|
|
38
|
+
[TIERS.READ]: 'auto',
|
|
39
|
+
[TIERS.SENSITIVE_READ]: 'prompt-explicit',
|
|
40
|
+
[TIERS.LOCAL_EDIT]: 'auto-with-undo',
|
|
41
|
+
[TIERS.SHELL_SAFE]: 'auto',
|
|
42
|
+
[TIERS.SHELL_MEDIUM]: 'prompt-safe',
|
|
43
|
+
[TIERS.SHELL_DANGEROUS]: 'prompt-explicit',
|
|
44
|
+
[TIERS.DESTRUCTIVE]: 'prompt-explicit',
|
|
45
|
+
[TIERS.NETWORK]: 'prompt-safe',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function behavior(tier) {
|
|
49
|
+
return BEHAVIOR[tier] || 'prompt-safe';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Tool → tier (non-shell) ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const READ_TOOLS = new Set([
|
|
55
|
+
'read_file', 'read_files',
|
|
56
|
+
'search_code', 'search_files', 'grep',
|
|
57
|
+
'list_files', 'get_file_info', 'get_project_overview',
|
|
58
|
+
'git_status', 'git_diff',
|
|
59
|
+
'analyze_code',
|
|
60
|
+
'validate_file', 'validate_structure',
|
|
61
|
+
'agents_list', 'workflow_list',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const READ_PATH_KEYS = [
|
|
65
|
+
'path', 'file_path', 'file', 'target',
|
|
66
|
+
'pattern', 'glob',
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const READ_PATH_ARRAY_KEYS = [
|
|
70
|
+
'paths', 'file_paths', 'files', 'targets', 'patterns', 'globs',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const LOCAL_EDIT_TOOLS = new Set([
|
|
74
|
+
'edit_file', 'write_file', 'write_project',
|
|
75
|
+
'skill_install', 'skill_update',
|
|
76
|
+
'agent_create', 'agent_sync',
|
|
77
|
+
'workflow_create_multi', 'workflow_sync_multi',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const DESTRUCTIVE_TOOLS = new Set([
|
|
81
|
+
'delete_file', 'skill_remove',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const NETWORK_TOOLS = new Set([
|
|
85
|
+
'WebFetch', 'fetch_url',
|
|
86
|
+
'workflow_run_multi',
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// ── Shell sub-classifier ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const SHELL_SAFE_RE = [
|
|
92
|
+
// Inspection / read-only + harmless shell navigation built-ins.
|
|
93
|
+
// `cd` / `pushd` / `popd` only change the process working directory; if
|
|
94
|
+
// chained with something dangerous, the multi-segment classifier still
|
|
95
|
+
// catches the danger (`cd /x && rm -rf .` → SHELL_DANGEROUS).
|
|
96
|
+
/^\s*(cd|pushd|popd|ls|cat|head|tail|less|more|wc|file|stat|tree|find|grep|rg|ag|fd|sed|echo|printf|pwd|whoami|date|which|type|env|printenv|uname|hostname|id|df|du|uptime|free|top|ps|lsof)\b/i,
|
|
97
|
+
// mkdir -p / touch are creation primitives but harmless in scope.
|
|
98
|
+
/^\s*mkdir\s+-p\b/i,
|
|
99
|
+
/^\s*touch\s/i,
|
|
100
|
+
/^\s*git\s+(status|log|diff|show|branch|tag|remote|stash\s+list|blame|shortlog|describe|rev-parse|ls-files|ls-tree|config\s+--get)\b/i,
|
|
101
|
+
// Test-only invocations
|
|
102
|
+
/^\s*(npm|pnpm|yarn)\s+(test|run\s+test|run\s+lint|list|ls|view|info|outdated)\b/i,
|
|
103
|
+
/^\s*node\s+--check\b/i,
|
|
104
|
+
/^\s*python3?\s+-m\s+py_compile\b/i,
|
|
105
|
+
/^\s*pytest\b(?!.*--?(delete|rm|destructive))/i,
|
|
106
|
+
/^\s*cargo\s+(check|test|clippy|build)\b/i,
|
|
107
|
+
/^\s*go\s+(test|vet|build)\b/i,
|
|
108
|
+
/^\s*make\s+(test|check|lint|build)\b/i,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const SHELL_DANGEROUS_RE = [
|
|
112
|
+
/\brm\s/i,
|
|
113
|
+
/\brm\s+-r/i,
|
|
114
|
+
/\brm\s+--recursive/i,
|
|
115
|
+
/\brm\s+-rf?\b/i,
|
|
116
|
+
/\bunlink\s/i,
|
|
117
|
+
/\brmdir\s+-/i,
|
|
118
|
+
/\bgit\s+push.*--force/i,
|
|
119
|
+
/\bgit\s+push.*-f\b/i,
|
|
120
|
+
/\bgit\s+reset\s+--hard/i,
|
|
121
|
+
/\bgit\s+clean\s+-f/i,
|
|
122
|
+
/\bgit\s+checkout\s+\./i,
|
|
123
|
+
/\bgit\s+stash\s+drop/i,
|
|
124
|
+
/\bgit\s+branch\s+-D/i,
|
|
125
|
+
/\bgit\s+filter-branch/i,
|
|
126
|
+
/\bsudo\b/i,
|
|
127
|
+
/\bsu\s+-/i,
|
|
128
|
+
/\bcurl\b.*\|\s*(sh|bash|zsh)/i,
|
|
129
|
+
/\bwget\b.*\|\s*(sh|bash|zsh)/i,
|
|
130
|
+
/\beval\s+["'$(]/i,
|
|
131
|
+
/\bfind\s+\/(?:\s|$)/i,
|
|
132
|
+
/\bfind\b.*\s-(?:exec|execdir|ok|okdir|delete|fprint|fprint0|fls|fprintf)\b/i,
|
|
133
|
+
/\bkubectl\s+delete/i,
|
|
134
|
+
/\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm|network\s+rm)/i,
|
|
135
|
+
/\bdrop\s+(table|database|schema)/i,
|
|
136
|
+
/\btruncate\s+table/i,
|
|
137
|
+
/\bmkfs\b/i,
|
|
138
|
+
/\bdd\s+if=/i,
|
|
139
|
+
/:\s*\(\s*\)\s*\{.*:\|/i, // fork bomb
|
|
140
|
+
/>\s*\/dev\/sda/i,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const SHELL_MEDIUM_RE = [
|
|
144
|
+
/^\s*(npm|pnpm|yarn)\s+(install|i|add|remove|uninstall|update|upgrade|publish|deploy)\b/i,
|
|
145
|
+
/^\s*pip\s+(install|uninstall|--upgrade)\b/i,
|
|
146
|
+
/^\s*pipx\s+(install|uninstall)\b/i,
|
|
147
|
+
/^\s*brew\s+(install|uninstall|upgrade|update)\b/i,
|
|
148
|
+
/^\s*apt(-get)?\s+(install|remove|upgrade|update)\b/i,
|
|
149
|
+
/^\s*cargo\s+(install|uninstall|publish|run)\b/i,
|
|
150
|
+
/^\s*go\s+(install|get|mod\s+tidy|mod\s+download)\b/i,
|
|
151
|
+
/^\s*make(\s|$)/i,
|
|
152
|
+
/^\s*git\s+(commit|push|pull|merge|rebase|fetch|checkout(?!\s+\.)|cherry-pick|revert|tag|stash(?!\s+drop))/i,
|
|
153
|
+
/^\s*docker\s+(build|run|exec|compose|pull|push|tag)/i,
|
|
154
|
+
/^\s*sed\b.*\s-i\S*(?:\s|$)/i,
|
|
155
|
+
/^\s*sed\b.*\s--in-place(?:=|\s|$)/i,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
export function classifyShell(command) {
|
|
159
|
+
const cmd = String(command || '').trim();
|
|
160
|
+
if (!cmd) return TIERS.SHELL_MEDIUM;
|
|
161
|
+
|
|
162
|
+
// Dangerous wins over safe — never let a safe-looking prefix mask `&& rm -rf`.
|
|
163
|
+
if (SHELL_DANGEROUS_RE.some(re => re.test(cmd))) return TIERS.SHELL_DANGEROUS;
|
|
164
|
+
|
|
165
|
+
if (hasUnsafeOutputRedirection(cmd)) return TIERS.SHELL_MEDIUM;
|
|
166
|
+
|
|
167
|
+
// For a chained command, classify each segment and take the riskiest —
|
|
168
|
+
// never let a safe-looking prefix mask `&& npm install` or worse.
|
|
169
|
+
if (/&&|\|\||;|\|(?!\|)/.test(cmd)) {
|
|
170
|
+
const segments = splitShellSegments(cmd);
|
|
171
|
+
if (segments.length > 1) {
|
|
172
|
+
let worst = TIERS.SHELL_SAFE;
|
|
173
|
+
for (const seg of segments) {
|
|
174
|
+
const t = classifyShell(seg);
|
|
175
|
+
worst = riskier(worst, t);
|
|
176
|
+
if (worst === TIERS.SHELL_DANGEROUS) return worst;
|
|
177
|
+
}
|
|
178
|
+
return worst;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (SHELL_MEDIUM_RE.some(re => re.test(cmd))) return TIERS.SHELL_MEDIUM;
|
|
183
|
+
if (SHELL_SAFE_RE.some(re => re.test(cmd))) return TIERS.SHELL_SAFE;
|
|
184
|
+
return TIERS.SHELL_MEDIUM;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hasUnsafeOutputRedirection(cmd) {
|
|
188
|
+
let inSingle = false;
|
|
189
|
+
let inDouble = false;
|
|
190
|
+
let escaped = false;
|
|
191
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
192
|
+
const ch = cmd[i];
|
|
193
|
+
if (escaped) { escaped = false; continue; }
|
|
194
|
+
if (ch === '\\' && !inSingle) { escaped = true; continue; }
|
|
195
|
+
if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
|
|
196
|
+
if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
|
|
197
|
+
if (inSingle || inDouble || ch !== '>') continue;
|
|
198
|
+
|
|
199
|
+
const prev = cmd[i - 1] || '';
|
|
200
|
+
const next = cmd[i + 1] || '';
|
|
201
|
+
if (next === '&') continue;
|
|
202
|
+
let cursor = next === '>' ? i + 2 : i + 1;
|
|
203
|
+
while (/\s/.test(cmd[cursor] || '')) cursor++;
|
|
204
|
+
const target = cmd.slice(cursor).match(/^[^\s;&|]+/)?.[0] || '';
|
|
205
|
+
if (target === '/dev/null') continue;
|
|
206
|
+
if (prev === '2' && target === '/dev/null') continue;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function splitShellSegments(cmd) {
|
|
213
|
+
// Split on top-level &&, ||, ;, | — naive but enough for the classifier.
|
|
214
|
+
return cmd.split(/&&|\|\||;|\|/).map(s => s.trim()).filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const TIER_ORDER = [
|
|
218
|
+
TIERS.READ,
|
|
219
|
+
TIERS.SENSITIVE_READ,
|
|
220
|
+
TIERS.SHELL_SAFE,
|
|
221
|
+
TIERS.LOCAL_EDIT,
|
|
222
|
+
TIERS.NETWORK,
|
|
223
|
+
TIERS.SHELL_MEDIUM,
|
|
224
|
+
TIERS.DESTRUCTIVE,
|
|
225
|
+
TIERS.SHELL_DANGEROUS,
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
function riskier(a, b) {
|
|
229
|
+
return TIER_ORDER.indexOf(b) > TIER_ORDER.indexOf(a) ? b : a;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Top-level classify ──────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Classify a tool call into a risk tier. Always returns one of TIERS.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} tool Tool name (e.g. 'shell', 'edit_file')
|
|
238
|
+
* @param {object} args Tool arguments (e.g. { command: 'rm -rf x' })
|
|
239
|
+
*/
|
|
240
|
+
export function classify(tool, args = {}) {
|
|
241
|
+
if (!tool) return TIERS.SHELL_MEDIUM;
|
|
242
|
+
|
|
243
|
+
if (READ_TOOLS.has(tool)) {
|
|
244
|
+
return isSensitiveRead(args) ? TIERS.SENSITIVE_READ : TIERS.READ;
|
|
245
|
+
}
|
|
246
|
+
if (LOCAL_EDIT_TOOLS.has(tool)) return TIERS.LOCAL_EDIT;
|
|
247
|
+
if (DESTRUCTIVE_TOOLS.has(tool)) return TIERS.DESTRUCTIVE;
|
|
248
|
+
if (NETWORK_TOOLS.has(tool)) return TIERS.NETWORK;
|
|
249
|
+
|
|
250
|
+
if (tool === 'shell' || tool === 'run_tests' || tool === 'validate_build' || tool === 'lint_check') {
|
|
251
|
+
return classifyShell(args.command || args.cmd || '');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Sub-agents inherit their parent's risk (read-ish by default).
|
|
255
|
+
if (['explore', 'plan', 'verify', 'debug', 'refactor', 'analyze_code'].includes(tool)) {
|
|
256
|
+
return TIERS.READ;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// MCP tools: assume network unless the name implies read.
|
|
260
|
+
if (tool.startsWith('mcp')) {
|
|
261
|
+
return /(?:read|get|list|search|describe|info|status)/i.test(tool) ? TIERS.READ : TIERS.NETWORK;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return TIERS.SHELL_MEDIUM;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Convenience: human label for a tier (used in approval prompts and the
|
|
269
|
+
* status bar). Returned strings already uppercased / hyphenated.
|
|
270
|
+
*/
|
|
271
|
+
export function label(tier) {
|
|
272
|
+
switch (tier) {
|
|
273
|
+
case TIERS.READ: return 'READ';
|
|
274
|
+
case TIERS.SENSITIVE_READ: return 'SENSITIVE-READ';
|
|
275
|
+
case TIERS.LOCAL_EDIT: return 'LOCAL-EDIT';
|
|
276
|
+
case TIERS.SHELL_SAFE: return 'SHELL-SAFE';
|
|
277
|
+
case TIERS.SHELL_MEDIUM: return 'SHELL-MEDIUM';
|
|
278
|
+
case TIERS.SHELL_DANGEROUS: return 'SHELL-DANGEROUS';
|
|
279
|
+
case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
|
|
280
|
+
case TIERS.NETWORK: return 'NETWORK';
|
|
281
|
+
default: return String(tier || '').toUpperCase();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Whether the tool needs an explicit human keystroke before running.
|
|
287
|
+
*/
|
|
288
|
+
export function requiresExplicitApproval(tier) {
|
|
289
|
+
return behavior(tier) === 'prompt-explicit';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Whether the tier should auto-create an undo checkpoint before running.
|
|
294
|
+
*/
|
|
295
|
+
export function requiresCheckpoint(tier) {
|
|
296
|
+
return behavior(tier) === 'auto-with-undo';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function isSensitiveRead(args = {}) {
|
|
300
|
+
return extractReadPaths(args).some(isSensitiveReadPath);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function isSensitiveReadPath(filePath = '') {
|
|
304
|
+
const normalized = String(filePath || '')
|
|
305
|
+
.replace(/\\/g, '/')
|
|
306
|
+
.replace(/\/+/g, '/')
|
|
307
|
+
.replace(/^\.\//, '')
|
|
308
|
+
.trim();
|
|
309
|
+
if (!normalized) return false;
|
|
310
|
+
|
|
311
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
312
|
+
const base = parts[parts.length - 1] || normalized;
|
|
313
|
+
if (base === '.env') return true;
|
|
314
|
+
if (/\.pem$/i.test(base)) return true;
|
|
315
|
+
return parts.includes('secrets');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function extractReadPaths(args = {}) {
|
|
319
|
+
const paths = [];
|
|
320
|
+
for (const key of READ_PATH_KEYS) {
|
|
321
|
+
if (typeof args[key] === 'string') paths.push(args[key]);
|
|
322
|
+
}
|
|
323
|
+
for (const key of READ_PATH_ARRAY_KEYS) {
|
|
324
|
+
const value = args[key];
|
|
325
|
+
if (!Array.isArray(value)) continue;
|
|
326
|
+
for (const item of value) {
|
|
327
|
+
if (typeof item === 'string') {
|
|
328
|
+
paths.push(item);
|
|
329
|
+
} else if (item && typeof item === 'object') {
|
|
330
|
+
for (const nestedKey of READ_PATH_KEYS) {
|
|
331
|
+
if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return paths;
|
|
337
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safety Guardrails — prevent destructive tool execution.
|
|
3
|
+
*
|
|
4
|
+
* Protects against:
|
|
5
|
+
* - Deleting/overwriting source code directories
|
|
6
|
+
* - rm -rf on critical paths
|
|
7
|
+
* - Writing outside allowed boundaries
|
|
8
|
+
* - Dangerous shell commands (fork bombs, disk wipes, etc.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
|
|
14
|
+
// ── Protected Paths ──────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Files/dirs that should NEVER be deleted or overwritten wholesale. */
|
|
17
|
+
const PROTECTED_NAMES = new Set([
|
|
18
|
+
'.git',
|
|
19
|
+
'.env',
|
|
20
|
+
'.env.local',
|
|
21
|
+
'.env.production',
|
|
22
|
+
'node_modules',
|
|
23
|
+
'package.json',
|
|
24
|
+
'package-lock.json',
|
|
25
|
+
'yarn.lock',
|
|
26
|
+
'pnpm-lock.yaml',
|
|
27
|
+
'Cargo.lock',
|
|
28
|
+
'go.sum',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/** Directory names that indicate a source root — never delete the dir itself. */
|
|
32
|
+
const SOURCE_DIRS = new Set([
|
|
33
|
+
'src',
|
|
34
|
+
'lib',
|
|
35
|
+
'app',
|
|
36
|
+
'pages',
|
|
37
|
+
'components',
|
|
38
|
+
'packages',
|
|
39
|
+
'dist',
|
|
40
|
+
'build',
|
|
41
|
+
'test',
|
|
42
|
+
'tests',
|
|
43
|
+
'__tests__',
|
|
44
|
+
'spec',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Shell patterns that are always blocked. */
|
|
48
|
+
const DANGEROUS_SHELL_PATTERNS = [
|
|
49
|
+
/rm\s+(-\w*[rR]\w*\s+)*(?:\/(?:\s|$)|~(?:\s|\/?\s*$)|\$HOME(?:\s|\/?\s*$)|\.\.(?:\s|\/|$)|\.\/\.\.)/, // rm -rf /, ~, $HOME, or ..
|
|
50
|
+
/rm\s+(-\w*[rR]\w*\s+)*\.\s*$/, // rm -rf .
|
|
51
|
+
/rm\s+(-\w*[rR]\w*\s+)*\*\s*$/, // rm -rf *
|
|
52
|
+
/:\(\)\s*\{\s*:\|\s*:\s*&\s*\}\s*;/, // fork bomb
|
|
53
|
+
/mkfs\./, // format filesystem
|
|
54
|
+
/dd\s+.*of=\/dev\//, // disk wipe
|
|
55
|
+
/>\s*\/dev\/sd/, // overwrite disk
|
|
56
|
+
/chmod\s+(-\w+\s+)*777\s+\//, // chmod 777 /
|
|
57
|
+
/curl.*\|\s*(ba)?sh/, // pipe curl to shell
|
|
58
|
+
/wget.*\|\s*(ba)?sh/, // pipe wget to shell
|
|
59
|
+
/eval\s*\$\(/, // eval command substitution
|
|
60
|
+
/\bfind\s+\/\s/, // find / (scans entire filesystem)
|
|
61
|
+
/\bfind\s+\/\s*$/, // find / at end
|
|
62
|
+
/\bls\s+(-\w+\s+)*\/\s*$/, // ls / (root listing)
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Commands that need extra scrutiny — require approval even if auto-approved. */
|
|
66
|
+
const HIGH_RISK_COMMANDS = [
|
|
67
|
+
/\brm\s/, // ALL rm commands require explicit approval
|
|
68
|
+
/\bunlink\s/, // unlink
|
|
69
|
+
/\brmdir\s/, // rmdir
|
|
70
|
+
/\bkill\s/, // process termination requires explicit approval
|
|
71
|
+
/\bpkill\s/, // process termination by pattern requires explicit approval
|
|
72
|
+
/\bxargs\s+kill\b/, // process termination via piped pid list
|
|
73
|
+
/git\s+push\s+--force/,
|
|
74
|
+
/git\s+reset\s+--hard/,
|
|
75
|
+
/git\s+clean\s+-[fd]/,
|
|
76
|
+
/drop\s+table/i,
|
|
77
|
+
/drop\s+database/i,
|
|
78
|
+
/truncate\s+table/i,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// ── Validation Functions ─────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a path is protected (should not be deleted/overwritten entirely).
|
|
85
|
+
* @param {string} filePath - Absolute path
|
|
86
|
+
* @param {string} cwd - Current working directory
|
|
87
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
88
|
+
*/
|
|
89
|
+
export function validatePath(filePath, cwd = process.cwd()) {
|
|
90
|
+
if (!filePath) return { safe: false, reason: 'Empty path' };
|
|
91
|
+
|
|
92
|
+
const resolved = path.resolve(cwd, filePath);
|
|
93
|
+
const basename = path.basename(resolved);
|
|
94
|
+
const relative = path.relative(cwd, resolved);
|
|
95
|
+
|
|
96
|
+
// Block anything outside CWD parent
|
|
97
|
+
const cwdParent = path.dirname(cwd);
|
|
98
|
+
if (!resolved.startsWith(cwdParent)) {
|
|
99
|
+
return { safe: false, reason: `Path outside workspace: ${filePath}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Block protected file names at any level
|
|
103
|
+
if (PROTECTED_NAMES.has(basename) && !resolved.includes('node_modules/')) {
|
|
104
|
+
return { safe: false, reason: `Protected path: ${basename}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { safe: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if a delete operation is safe.
|
|
112
|
+
* Stricter than validatePath — also blocks source directories.
|
|
113
|
+
* @param {string} filePath - Path to delete
|
|
114
|
+
* @param {string} cwd - Current working directory
|
|
115
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
116
|
+
*/
|
|
117
|
+
export function validateDelete(filePath, cwd = process.cwd()) {
|
|
118
|
+
const pathCheck = validatePath(filePath, cwd);
|
|
119
|
+
if (!pathCheck.safe) return pathCheck;
|
|
120
|
+
|
|
121
|
+
const resolved = path.resolve(cwd, filePath);
|
|
122
|
+
const basename = path.basename(resolved);
|
|
123
|
+
|
|
124
|
+
// Never delete source root directories
|
|
125
|
+
if (SOURCE_DIRS.has(basename)) {
|
|
126
|
+
try {
|
|
127
|
+
const stat = fs.statSync(resolved);
|
|
128
|
+
if (stat.isDirectory()) {
|
|
129
|
+
return { safe: false, reason: `Cannot delete source directory: ${basename}/` };
|
|
130
|
+
}
|
|
131
|
+
} catch { /* file doesn't exist, safe to proceed */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Never delete CWD itself
|
|
135
|
+
if (resolved === cwd || resolved === path.dirname(cwd)) {
|
|
136
|
+
return { safe: false, reason: 'Cannot delete working directory' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { safe: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a shell command is safe to execute.
|
|
144
|
+
* @param {string} command
|
|
145
|
+
* @returns {{ safe: boolean, reason?: string, highRisk?: boolean }}
|
|
146
|
+
*/
|
|
147
|
+
export function validateShellCommand(command) {
|
|
148
|
+
if (!command) return { safe: false, reason: 'Empty command' };
|
|
149
|
+
|
|
150
|
+
// Always block dangerous patterns
|
|
151
|
+
for (const pattern of DANGEROUS_SHELL_PATTERNS) {
|
|
152
|
+
if (pattern.test(command)) {
|
|
153
|
+
return { safe: false, reason: `Blocked dangerous command: ${command.slice(0, 60)}` };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Flag high-risk commands (still allowed, but should force approval)
|
|
158
|
+
for (const pattern of HIGH_RISK_COMMANDS) {
|
|
159
|
+
if (pattern.test(command)) {
|
|
160
|
+
return { safe: true, highRisk: true, reason: `High-risk command: ${command.slice(0, 60)}` };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { safe: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if a write operation targets a sensible path.
|
|
169
|
+
* @param {string} filePath
|
|
170
|
+
* @param {string} content
|
|
171
|
+
* @param {string} cwd
|
|
172
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
173
|
+
*/
|
|
174
|
+
export function validateWrite(filePath, content, cwd = process.cwd()) {
|
|
175
|
+
const pathCheck = validatePath(filePath, cwd);
|
|
176
|
+
if (!pathCheck.safe) return pathCheck;
|
|
177
|
+
|
|
178
|
+
const resolved = path.resolve(cwd, filePath);
|
|
179
|
+
|
|
180
|
+
// Don't allow overwriting .git internals
|
|
181
|
+
if (resolved.includes('/.git/')) {
|
|
182
|
+
return { safe: false, reason: 'Cannot write to .git directory' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Warn if writing a very large file (> 1MB)
|
|
186
|
+
if (content && content.length > 1_000_000) {
|
|
187
|
+
return { safe: true, reason: `Large file write: ${(content.length / 1024).toFixed(0)}KB` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { safe: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get a human-readable summary of safety rules.
|
|
195
|
+
*/
|
|
196
|
+
export function getSafetyRules() {
|
|
197
|
+
return {
|
|
198
|
+
protectedNames: [...PROTECTED_NAMES],
|
|
199
|
+
sourceDirs: [...SOURCE_DIRS],
|
|
200
|
+
blockedPatterns: DANGEROUS_SHELL_PATTERNS.length,
|
|
201
|
+
highRiskPatterns: HIGH_RISK_COMMANDS.length,
|
|
202
|
+
};
|
|
203
|
+
}
|