@vibecodetown/mcp-server 2.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.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/build/auth/gate.js +225 -0
  4. package/build/auth/index.js +55 -0
  5. package/build/auth/public_key.js +27 -0
  6. package/build/auth/token_cache.js +122 -0
  7. package/build/auth/token_verifier.js +103 -0
  8. package/build/bootstrap/doctor.js +115 -0
  9. package/build/bootstrap/installer.js +673 -0
  10. package/build/bootstrap/lock.js +37 -0
  11. package/build/bootstrap/platform.js +26 -0
  12. package/build/bootstrap/registry.js +37 -0
  13. package/build/cache/index.js +147 -0
  14. package/build/cli.js +101 -0
  15. package/build/contracts.js +22 -0
  16. package/build/control_plane/gate.js +161 -0
  17. package/build/control_plane/index.js +6 -0
  18. package/build/dx/activity.js +139 -0
  19. package/build/engine.js +106 -0
  20. package/build/errors.js +171 -0
  21. package/build/generated/activate_input.js +2 -0
  22. package/build/generated/activate_output.js +57 -0
  23. package/build/generated/advisory_review_input.js +2 -0
  24. package/build/generated/advisory_review_output.js +35 -0
  25. package/build/generated/auth_token_file.js +2 -0
  26. package/build/generated/briefing_input.js +2 -0
  27. package/build/generated/briefing_output.js +2 -0
  28. package/build/generated/clinic_bridge_file.js +13 -0
  29. package/build/generated/contracts_bundle_info.js +5 -0
  30. package/build/generated/create_work_order_input.js +2 -0
  31. package/build/generated/create_work_order_output.js +2 -0
  32. package/build/generated/current_work_order_file.js +2 -0
  33. package/build/generated/doctor_input.js +2 -0
  34. package/build/generated/doctor_output.js +24 -0
  35. package/build/generated/execution_result.js +2 -0
  36. package/build/generated/execution_task.js +2 -0
  37. package/build/generated/export_output_input.js +2 -0
  38. package/build/generated/export_output_output.js +2 -0
  39. package/build/generated/finalize_work_input.js +2 -0
  40. package/build/generated/finalize_work_output.js +2 -0
  41. package/build/generated/gate_input.js +2 -0
  42. package/build/generated/gate_output.js +2 -0
  43. package/build/generated/gate_result_v1.js +2 -0
  44. package/build/generated/get_decision_input.js +2 -0
  45. package/build/generated/get_decision_output.js +13 -0
  46. package/build/generated/handoff_to_clinic.js +2 -0
  47. package/build/generated/index.js +75 -0
  48. package/build/generated/inspect_code_input.js +2 -0
  49. package/build/generated/inspect_code_output.js +13 -0
  50. package/build/generated/memory_retrieve_output.js +2 -0
  51. package/build/generated/memory_state_file.js +2 -0
  52. package/build/generated/memory_status_input.js +2 -0
  53. package/build/generated/memory_status_output.js +13 -0
  54. package/build/generated/memory_sync_input.js +2 -0
  55. package/build/generated/memory_sync_output.js +13 -0
  56. package/build/generated/plugin_result.js +2 -0
  57. package/build/generated/react_perf_check_patterns_input.js +2 -0
  58. package/build/generated/react_perf_check_patterns_output.js +2 -0
  59. package/build/generated/react_perf_generate_report_input.js +2 -0
  60. package/build/generated/react_perf_generate_report_output.js +2 -0
  61. package/build/generated/repair_plan_input.js +2 -0
  62. package/build/generated/repair_plan_output.js +2 -0
  63. package/build/generated/run_app_input.js +2 -0
  64. package/build/generated/run_app_output.js +2 -0
  65. package/build/generated/run_state_file.js +13 -0
  66. package/build/generated/scaffold_input.js +2 -0
  67. package/build/generated/scaffold_output.js +2 -0
  68. package/build/generated/search_oss_input.js +2 -0
  69. package/build/generated/search_oss_output.js +2 -0
  70. package/build/generated/selection_validation_result.js +2 -0
  71. package/build/generated/signal_agent_input.js +2 -0
  72. package/build/generated/spec_high_ask_queue_items_file.js +2 -0
  73. package/build/generated/spec_high_clinic_bridge_output.js +2 -0
  74. package/build/generated/spec_high_decision_draft_output.js +2 -0
  75. package/build/generated/spec_high_validate_output.js +2 -0
  76. package/build/generated/status_input.js +2 -0
  77. package/build/generated/status_output.js +2 -0
  78. package/build/generated/submit_decision_input.js +2 -0
  79. package/build/generated/submit_decision_output.js +2 -0
  80. package/build/generated/tool_error_output.js +2 -0
  81. package/build/generated/undo_last_task_input.js +2 -0
  82. package/build/generated/undo_last_task_output.js +2 -0
  83. package/build/generated/update_input.js +2 -0
  84. package/build/generated/update_output.js +2 -0
  85. package/build/generated/vibe_pm_inspection_result.js +2 -0
  86. package/build/generated/vibe_pm_report_markdown.js +2 -0
  87. package/build/generated/vibe_pm_verdict.js +2 -0
  88. package/build/generated/vibe_repo_config.js +2 -0
  89. package/build/generated/vibecoding_helper_answer_output.js +2 -0
  90. package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
  91. package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
  92. package/build/generated/work_order_v1.js +2 -0
  93. package/build/generated/zoekt_evidence_input.js +2 -0
  94. package/build/generated/zoekt_evidence_output.js +2 -0
  95. package/build/index.js +111 -0
  96. package/build/legacy_alias.js +65 -0
  97. package/build/local-mode/bash.js +61 -0
  98. package/build/local-mode/config.js +171 -0
  99. package/build/local-mode/git.js +33 -0
  100. package/build/local-mode/init.js +110 -0
  101. package/build/local-mode/paths.js +24 -0
  102. package/build/local-mode/templates.js +856 -0
  103. package/build/local-mode/work-order.js +41 -0
  104. package/build/resources/index.js +246 -0
  105. package/build/security/input-validator.js +119 -0
  106. package/build/security/path-policy.js +289 -0
  107. package/build/security/sandbox.js +228 -0
  108. package/build/tools/react_perf/check_patterns.js +172 -0
  109. package/build/tools/react_perf/generate_report.js +337 -0
  110. package/build/tools/react_perf/index.js +119 -0
  111. package/build/tools/react_perf/rules/advanced.js +325 -0
  112. package/build/tools/react_perf/rules/async.js +104 -0
  113. package/build/tools/react_perf/rules/bundle.js +101 -0
  114. package/build/tools/react_perf/rules/client.js +186 -0
  115. package/build/tools/react_perf/rules/index.js +74 -0
  116. package/build/tools/react_perf/rules/js.js +148 -0
  117. package/build/tools/react_perf/rules/rendering.js +166 -0
  118. package/build/tools/react_perf/rules/rerender.js +161 -0
  119. package/build/tools/react_perf/rules/server.js +141 -0
  120. package/build/tools/react_perf/types.js +127 -0
  121. package/build/tools/vibe_pm/activate.js +102 -0
  122. package/build/tools/vibe_pm/advisory_review.js +77 -0
  123. package/build/tools/vibe_pm/briefing.js +178 -0
  124. package/build/tools/vibe_pm/context.js +439 -0
  125. package/build/tools/vibe_pm/create_work_order.js +271 -0
  126. package/build/tools/vibe_pm/doc_status_gate.js +370 -0
  127. package/build/tools/vibe_pm/doctor.js +262 -0
  128. package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
  129. package/build/tools/vibe_pm/export_output.js +135 -0
  130. package/build/tools/vibe_pm/finalize_work.js +393 -0
  131. package/build/tools/vibe_pm/gate.js +33 -0
  132. package/build/tools/vibe_pm/get_decision.js +281 -0
  133. package/build/tools/vibe_pm/index.js +593 -0
  134. package/build/tools/vibe_pm/inspect_code.js +828 -0
  135. package/build/tools/vibe_pm/intent/generator.js +294 -0
  136. package/build/tools/vibe_pm/intent/index.js +5 -0
  137. package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
  138. package/build/tools/vibe_pm/intent/types.js +70 -0
  139. package/build/tools/vibe_pm/intent/verifier.js +237 -0
  140. package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
  141. package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
  142. package/build/tools/vibe_pm/kce/preflight.js +232 -0
  143. package/build/tools/vibe_pm/local_memory.js +26 -0
  144. package/build/tools/vibe_pm/memory_status.js +82 -0
  145. package/build/tools/vibe_pm/memory_sync.js +134 -0
  146. package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
  147. package/build/tools/vibe_pm/modules/ensure.js +100 -0
  148. package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
  149. package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
  150. package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
  151. package/build/tools/vibe_pm/modules/repo_context.js +56 -0
  152. package/build/tools/vibe_pm/modules/research_v1.js +114 -0
  153. package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
  154. package/build/tools/vibe_pm/pm_language.js +222 -0
  155. package/build/tools/vibe_pm/repair_plan.js +199 -0
  156. package/build/tools/vibe_pm/run_app.js +597 -0
  157. package/build/tools/vibe_pm/run_app_podman.js +64 -0
  158. package/build/tools/vibe_pm/scaffold.js +550 -0
  159. package/build/tools/vibe_pm/search_oss.js +124 -0
  160. package/build/tools/vibe_pm/status.js +153 -0
  161. package/build/tools/vibe_pm/submit_decision.js +87 -0
  162. package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
  163. package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
  164. package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
  165. package/build/tools/vibe_pm/types.js +229 -0
  166. package/build/tools/vibe_pm/undo_last_task.js +163 -0
  167. package/build/tools/vibe_pm/update.js +146 -0
  168. package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
  169. package/build/tools.js +269 -0
  170. package/build/version-check.js +239 -0
  171. package/build/vibe-cli.js +631 -0
  172. package/package.json +76 -0
@@ -0,0 +1,41 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { CurrentWorkOrderFileSchema } from "../generated/current_work_order_file.js";
4
+ export function readCurrentWorkOrder(filePath) {
5
+ if (!fs.existsSync(filePath))
6
+ return null;
7
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
8
+ return CurrentWorkOrderFileSchema.parse(raw);
9
+ }
10
+ export function createWorkOrderTemplate(topic) {
11
+ const now = new Date();
12
+ const ymd = now.toISOString().slice(0, 10).replace(/-/g, "");
13
+ const suffix = String(now.getTime() % 1000).padStart(3, "0");
14
+ return CurrentWorkOrderFileSchema.parse({
15
+ run_id: `wo-${ymd}-${suffix}`,
16
+ created_at: now.toISOString(),
17
+ topic: topic && topic.trim().length > 0 ? topic.trim() : "Work order created locally",
18
+ scope: {
19
+ include: ["src/**", "lib/**", "tests/**", "docs/**"],
20
+ exclude: [],
21
+ },
22
+ do_not_touch: ["config/**", ".env*", "secrets/**", "*.pem", "*.key"],
23
+ verify_criteria: [],
24
+ });
25
+ }
26
+ export function writeCurrentWorkOrder(filePath, workOrder, opts) {
27
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
+ if (fs.existsSync(filePath) && !opts?.overwrite) {
29
+ throw new Error(`Work order already exists: ${filePath} (use --force to overwrite)`);
30
+ }
31
+ fs.writeFileSync(filePath, JSON.stringify(workOrder, null, 2) + "\n", "utf-8");
32
+ }
33
+ export function archiveCurrentWorkOrder(workOrderFile, archiveDir) {
34
+ const wo = readCurrentWorkOrder(workOrderFile);
35
+ if (!wo)
36
+ return null;
37
+ fs.mkdirSync(archiveDir, { recursive: true });
38
+ const archivedPath = path.join(archiveDir, `${wo.run_id}.json`);
39
+ fs.renameSync(workOrderFile, archivedPath);
40
+ return { archivedPath };
41
+ }
@@ -0,0 +1,246 @@
1
+ // adapters/mcp-ts/src/resources/index.ts
2
+ // Resource templates for vibe:// URI scheme
3
+ import { z } from "zod";
4
+ import { existsSync, readFileSync, readdirSync } from "fs";
5
+ import { join, resolve } from "path";
6
+ // ============================================================
7
+ // Resource URI Templates
8
+ // ============================================================
9
+ /**
10
+ * vibe://project/{runId} - Project state resource
11
+ * vibe://project/{runId}/intent - Intent document resource
12
+ * vibe://project/{runId}/bridge - Verification bridge resource
13
+ * vibe://project/{runId}/decisions - Decisions history resource
14
+ */
15
+ // ============================================================
16
+ // Path Resolution Utilities
17
+ // ============================================================
18
+ function getRunsDir() {
19
+ // Default to current working directory's runs folder
20
+ return resolve(process.cwd(), "runs");
21
+ }
22
+ function getRunPath(runId) {
23
+ return join(getRunsDir(), runId);
24
+ }
25
+ function readJsonFile(filePath) {
26
+ if (!existsSync(filePath)) {
27
+ return null;
28
+ }
29
+ const content = readFileSync(filePath, "utf-8");
30
+ return JSON.parse(content);
31
+ }
32
+ function readYamlOrJson(basePath, filename) {
33
+ const jsonPath = join(basePath, `${filename}.json`);
34
+ const yamlPath = join(basePath, `${filename}.yaml`);
35
+ if (existsSync(jsonPath)) {
36
+ return readJsonFile(jsonPath);
37
+ }
38
+ if (existsSync(yamlPath)) {
39
+ const content = readFileSync(yamlPath, "utf-8");
40
+ // Simple YAML parsing for our use case (key: value format)
41
+ // For complex YAML, would need yaml library
42
+ return { raw: content, format: "yaml" };
43
+ }
44
+ return null;
45
+ }
46
+ // ============================================================
47
+ // Resource Content Schemas
48
+ // ============================================================
49
+ const projectStateSchema = z.object({
50
+ run_id: z.string(),
51
+ project_id: z.string(),
52
+ phase: z.string(),
53
+ mode: z.string(),
54
+ created_at: z.string(),
55
+ decisions_made: z.number(),
56
+ decisions_pending: z.number(),
57
+ last_review_status: z.enum(["GO", "FIX", "BLOCK"]).optional()
58
+ });
59
+ const intentDocumentSchema = z.object({
60
+ version: z.string(),
61
+ headline: z.string(),
62
+ scope: z.object({
63
+ include: z.array(z.string()),
64
+ exclude: z.array(z.string())
65
+ }),
66
+ verify_criteria: z.array(z.string()),
67
+ decisions: z.array(z.object({
68
+ decision_id: z.string(),
69
+ title: z.string(),
70
+ chosen: z.string()
71
+ }))
72
+ });
73
+ const bridgeSchema = z.object({
74
+ version: z.string().optional(),
75
+ intent: z.object({
76
+ headline: z.string(),
77
+ scope: z.object({
78
+ include: z.array(z.string()),
79
+ exclude: z.array(z.string())
80
+ }).optional()
81
+ }).optional(),
82
+ verify: z.object({
83
+ criteria: z.array(z.string()),
84
+ commands: z.array(z.string()).optional()
85
+ }).optional()
86
+ });
87
+ async function handleProjectState(runId) {
88
+ const runPath = getRunPath(runId);
89
+ const statePath = join(runPath, "state.json");
90
+ const state = readJsonFile(statePath);
91
+ if (!state) {
92
+ return {
93
+ contents: [{
94
+ uri: `vibe://project/${runId}`,
95
+ mimeType: "application/json",
96
+ text: JSON.stringify({
97
+ error: "NOT_FOUND",
98
+ message: `Run "${runId}" not found`,
99
+ run_id: runId
100
+ }, null, 2)
101
+ }]
102
+ };
103
+ }
104
+ return {
105
+ contents: [{
106
+ uri: `vibe://project/${runId}`,
107
+ mimeType: "application/json",
108
+ text: JSON.stringify(state, null, 2)
109
+ }]
110
+ };
111
+ }
112
+ async function handleProjectIntent(runId) {
113
+ const runPath = getRunPath(runId);
114
+ const intentDir = join(runPath, "intent");
115
+ // Try to find intent document
116
+ const intent = readYamlOrJson(intentDir, "intent") ||
117
+ readYamlOrJson(runPath, "intent");
118
+ if (!intent) {
119
+ return {
120
+ contents: [{
121
+ uri: `vibe://project/${runId}/intent`,
122
+ mimeType: "application/json",
123
+ text: JSON.stringify({
124
+ error: "NOT_FOUND",
125
+ message: `Intent document not found for run "${runId}"`,
126
+ run_id: runId
127
+ }, null, 2)
128
+ }]
129
+ };
130
+ }
131
+ return {
132
+ contents: [{
133
+ uri: `vibe://project/${runId}/intent`,
134
+ mimeType: "application/json",
135
+ text: JSON.stringify(intent, null, 2)
136
+ }]
137
+ };
138
+ }
139
+ async function handleProjectBridge(runId) {
140
+ const runPath = getRunPath(runId);
141
+ const handoffDir = join(runPath, "handoff");
142
+ const bridge = readYamlOrJson(handoffDir, "clinic_bridge") ||
143
+ readYamlOrJson(runPath, "clinic_bridge");
144
+ if (!bridge) {
145
+ return {
146
+ contents: [{
147
+ uri: `vibe://project/${runId}/bridge`,
148
+ mimeType: "application/json",
149
+ text: JSON.stringify({
150
+ error: "NOT_FOUND",
151
+ message: `Bridge file not found for run "${runId}"`,
152
+ run_id: runId,
153
+ hint: "Run vibe_pm.create_work_order to generate bridge file"
154
+ }, null, 2)
155
+ }]
156
+ };
157
+ }
158
+ return {
159
+ contents: [{
160
+ uri: `vibe://project/${runId}/bridge`,
161
+ mimeType: "application/json",
162
+ text: JSON.stringify(bridge, null, 2)
163
+ }]
164
+ };
165
+ }
166
+ async function handleProjectDecisions(runId) {
167
+ const runPath = getRunPath(runId);
168
+ const decisionsPath = join(runPath, "decisions.json");
169
+ const decisions = readJsonFile(decisionsPath);
170
+ if (!decisions) {
171
+ return {
172
+ contents: [{
173
+ uri: `vibe://project/${runId}/decisions`,
174
+ mimeType: "application/json",
175
+ text: JSON.stringify({
176
+ error: "NOT_FOUND",
177
+ message: `Decisions not found for run "${runId}"`,
178
+ run_id: runId,
179
+ decisions: []
180
+ }, null, 2)
181
+ }]
182
+ };
183
+ }
184
+ return {
185
+ contents: [{
186
+ uri: `vibe://project/${runId}/decisions`,
187
+ mimeType: "application/json",
188
+ text: JSON.stringify(decisions, null, 2)
189
+ }]
190
+ };
191
+ }
192
+ // ============================================================
193
+ // Resource Registration
194
+ // ============================================================
195
+ /**
196
+ * Register all vibe:// resource templates
197
+ */
198
+ export function registerVibeResources(server) {
199
+ // List available runs as resources
200
+ server.resource("vibe-projects", "vibe://projects", { mimeType: "application/json", description: "List all available Vibe PM runs" }, async () => {
201
+ const runsDir = getRunsDir();
202
+ let runs = [];
203
+ if (existsSync(runsDir)) {
204
+ runs = readdirSync(runsDir, { withFileTypes: true })
205
+ .filter(d => d.isDirectory())
206
+ .map(d => d.name)
207
+ .sort()
208
+ .reverse(); // Latest first
209
+ }
210
+ return {
211
+ contents: [{
212
+ uri: "vibe://projects",
213
+ mimeType: "application/json",
214
+ text: JSON.stringify({
215
+ runs_dir: runsDir,
216
+ runs: runs.map(runId => ({
217
+ run_id: runId,
218
+ uri: `vibe://project/${runId}`,
219
+ intent_uri: `vibe://project/${runId}/intent`,
220
+ bridge_uri: `vibe://project/${runId}/bridge`
221
+ }))
222
+ }, null, 2)
223
+ }]
224
+ };
225
+ });
226
+ }
227
+ /**
228
+ * Register dynamic resources based on available runs
229
+ *
230
+ * Note: MCP SDK doesn't support resource templates directly.
231
+ * Resources are registered dynamically for each run found.
232
+ * Call this after runs are created to register new resources.
233
+ */
234
+ export function registerVibeResourceTemplates(_server) {
235
+ // Resource templates are not supported in the current MCP SDK version.
236
+ // Dynamic resources should be registered using the resource() method
237
+ // when runs are discovered. This is a placeholder for future SDK support.
238
+ //
239
+ // To access run-specific resources, use:
240
+ // - vibe://projects to list all runs
241
+ // - Tools like vibe_pm.status to get run details
242
+ }
243
+ // ============================================================
244
+ // Export
245
+ // ============================================================
246
+ export { projectStateSchema, intentDocumentSchema, bridgeSchema, getRunsDir, getRunPath };
@@ -0,0 +1,119 @@
1
+ // adapters/mcp-ts/src/security/input-validator.ts
2
+ // Input validation for tool inputs
3
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
4
+ /**
5
+ * Input size limits for various fields
6
+ */
7
+ export const INPUT_LIMITS = {
8
+ projectBrief: 50_000,
9
+ additionalInstructions: 10_000,
10
+ targetPathsCount: 100,
11
+ singlePathLength: 500,
12
+ customInput: 5_000,
13
+ decisionNote: 2_000,
14
+ runIdLength: 128,
15
+ projectIdLength: 128,
16
+ };
17
+ /**
18
+ * Security-related error class
19
+ */
20
+ export class SecurityError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(message);
24
+ this.code = code;
25
+ this.name = "SecurityError";
26
+ }
27
+ }
28
+ /**
29
+ * Validate string length against limits
30
+ *
31
+ * @throws McpError if validation fails
32
+ */
33
+ export function validateStringLength(value, field, fieldName) {
34
+ if (value === undefined)
35
+ return;
36
+ const limit = INPUT_LIMITS[field];
37
+ if (value.length > limit) {
38
+ throw inputTooLargeError(fieldName, limit, value.length);
39
+ }
40
+ }
41
+ /**
42
+ * Validate array size
43
+ *
44
+ * @throws McpError if validation fails
45
+ */
46
+ export function validateArraySize(arr, maxSize, fieldName) {
47
+ if (arr === undefined)
48
+ return;
49
+ if (arr.length > maxSize) {
50
+ throw inputTooLargeError(fieldName, maxSize, arr.length);
51
+ }
52
+ }
53
+ /**
54
+ * Validate each string in an array for length
55
+ */
56
+ export function validateStringArray(arr, maxLength, fieldName) {
57
+ if (arr === undefined)
58
+ return;
59
+ for (let i = 0; i < arr.length; i++) {
60
+ if (arr[i].length > maxLength) {
61
+ throw inputTooLargeError(`${fieldName}[${i}]`, maxLength, arr[i].length);
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Validate common tool input fields
67
+ *
68
+ * @throws McpError if any validation fails
69
+ */
70
+ export function validateToolInput(input) {
71
+ // Validate string fields
72
+ validateStringLength(input.project_brief, "projectBrief", "project_brief");
73
+ validateStringLength(input.additional_instructions, "additionalInstructions", "additional_instructions");
74
+ validateStringLength(input.note, "decisionNote", "note");
75
+ validateStringLength(input.run_id, "runIdLength", "run_id");
76
+ validateStringLength(input.project_id, "projectIdLength", "project_id");
77
+ validateStringLength(input.custom_input, "customInput", "custom_input");
78
+ // Validate target_paths array
79
+ if (input.target_paths) {
80
+ validateArraySize(input.target_paths, INPUT_LIMITS.targetPathsCount, "target_paths");
81
+ validateStringArray(input.target_paths, INPUT_LIMITS.singlePathLength, "target_paths");
82
+ }
83
+ }
84
+ /**
85
+ * Validate briefing-specific input
86
+ */
87
+ export function validateBriefingInput(input) {
88
+ validateStringLength(input.project_brief, "projectBrief", "project_brief");
89
+ validateStringLength(input.project_id, "projectIdLength", "project_id");
90
+ // project_brief is required and must not be empty
91
+ if (!input.project_brief || input.project_brief.trim().length === 0) {
92
+ throw new McpError(ErrorCode.InvalidParams, "[VALIDATION] project_brief is required and must not be empty");
93
+ }
94
+ }
95
+ /**
96
+ * Validate inspect_code-specific input
97
+ */
98
+ export function validateInspectCodeInput(input) {
99
+ if (input.target_paths) {
100
+ validateArraySize(input.target_paths, INPUT_LIMITS.targetPathsCount, "target_paths");
101
+ validateStringArray(input.target_paths, INPUT_LIMITS.singlePathLength, "target_paths");
102
+ }
103
+ if (input.bridge_path) {
104
+ validateStringLength(input.bridge_path, "singlePathLength", "bridge_path");
105
+ }
106
+ if (input.run_id) {
107
+ validateStringLength(input.run_id, "runIdLength", "run_id");
108
+ }
109
+ }
110
+ /**
111
+ * Create InputTooLarge error
112
+ */
113
+ export function inputTooLargeError(field, limit, actual) {
114
+ return new McpError(ErrorCode.InvalidParams, `[SECURITY] Input too large: ${field} exceeds limit (${actual} > ${limit})`, {
115
+ vibeCode: -32_062,
116
+ category: "SECURITY",
117
+ context: { field, limit, actual },
118
+ });
119
+ }
@@ -0,0 +1,289 @@
1
+ // adapters/mcp-ts/src/security/path-policy.ts
2
+ // Path validation and do_not_touch policy enforcement
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
6
+ import { minimatch } from "minimatch";
7
+ // ============================================================
8
+ // Vibe-SDS Path Zones (Standard Directory Structure)
9
+ // ============================================================
10
+ /**
11
+ * Path zones for AI workspace control
12
+ * - BLACK: System only - AI cannot read or write
13
+ * - RED: Protected - AI cannot modify (auto do_not_touch)
14
+ * - YELLOW: Caution - AI can modify with warnings
15
+ * - GREEN: Free zone - AI workspace
16
+ */
17
+ export const PATH_ZONES = {
18
+ /** System state - AI cannot access */
19
+ BLACK: [".vibe", ".vibe/**"],
20
+ /** Protected config - auto do_not_touch */
21
+ RED: ["config/**", ".env", ".env.*", "*.pem", "*.key", "credentials.*"],
22
+ /** Documentation - AI can modify with caution */
23
+ YELLOW: ["docs/**", "README.md", "*.md"],
24
+ /** AI workspace - free to modify */
25
+ GREEN: ["src/**", "tests/**", "lib/**", "scripts/**"],
26
+ };
27
+ const CONTROL_PLANE_BLACKLIST = [
28
+ "vibecoding_helper/**",
29
+ "adapters/**",
30
+ "engines/**",
31
+ "schemas/**",
32
+ "fixtures/**",
33
+ "policy/**",
34
+ "runs/**",
35
+ "config/semgrep/**",
36
+ "config/gitleaks/**",
37
+ "docs/ssot/**",
38
+ "docs/DEV_SPEC/implemented/**",
39
+ "scripts/generate-contracts.sh",
40
+ "scripts/generate-contract-lock.py",
41
+ "schemas/contracts.version.json",
42
+ "schemas/contracts.lock.json",
43
+ ];
44
+ let cachedControlPlaneRepo = null;
45
+ function isControlPlaneRepo() {
46
+ if (cachedControlPlaneRepo !== null)
47
+ return cachedControlPlaneRepo;
48
+ const cwd = process.cwd();
49
+ const hasSpec = fs.existsSync(path.join(cwd, "vibecoding_helper.spec"));
50
+ const hasLayout = fs.existsSync(path.join(cwd, "vibecoding_helper")) && fs.existsSync(path.join(cwd, "adapters", "mcp-ts"));
51
+ cachedControlPlaneRepo = hasSpec || hasLayout;
52
+ return cachedControlPlaneRepo;
53
+ }
54
+ /**
55
+ * Get the zone for a given path
56
+ */
57
+ export function getPathZone(relativePath) {
58
+ const normalized = relativePath.replace(/\\/g, "/");
59
+ if (isControlPlaneRepo()) {
60
+ for (const pattern of CONTROL_PLANE_BLACKLIST) {
61
+ if (minimatch(normalized, pattern, { dot: true })) {
62
+ return "BLACK";
63
+ }
64
+ }
65
+ }
66
+ // Check zones in priority order: BLACK > RED > YELLOW > GREEN
67
+ for (const [zone, patterns] of Object.entries(PATH_ZONES)) {
68
+ for (const pattern of patterns) {
69
+ if (minimatch(normalized, pattern, { dot: true })) {
70
+ return zone;
71
+ }
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Check if a path is in a forbidden zone (BLACK or RED)
78
+ */
79
+ export function isPathForbidden(relativePath) {
80
+ const zone = getPathZone(relativePath);
81
+ if (zone === "BLACK") {
82
+ return {
83
+ forbidden: true,
84
+ zone,
85
+ reason: "이 경로는 시스템 전용 영역입니다. AI가 접근할 수 없습니다.",
86
+ };
87
+ }
88
+ if (zone === "RED") {
89
+ return {
90
+ forbidden: true,
91
+ zone,
92
+ reason: "이 경로는 보호된 설정 파일입니다. 수정할 수 없습니다.",
93
+ };
94
+ }
95
+ return { forbidden: false, zone: zone ?? undefined };
96
+ }
97
+ export function validateWorkOrderPaths(relativePaths) {
98
+ const allowed = [];
99
+ const forbidden = [];
100
+ for (const p of relativePaths) {
101
+ const check = isPathForbidden(p);
102
+ if (check.forbidden && check.zone && check.reason) {
103
+ forbidden.push({ path: p, zone: check.zone, reason: check.reason });
104
+ }
105
+ else {
106
+ allowed.push(p);
107
+ }
108
+ }
109
+ return {
110
+ valid: forbidden.length === 0,
111
+ allowed,
112
+ forbidden,
113
+ };
114
+ }
115
+ // ============================================================
116
+ // Dangerous Patterns (system-level protection)
117
+ // ============================================================
118
+ /**
119
+ * Dangerous path patterns that should always be blocked
120
+ */
121
+ const DANGEROUS_PATTERNS = [
122
+ /\.\./, // Path traversal
123
+ /^\/etc\//, // System config
124
+ /^\/usr\//, // System binaries
125
+ /^\/bin\//, // Binaries
126
+ /^\/sbin\//, // System binaries
127
+ /^\/var\/log\//, // System logs
128
+ /^\/root\//, // Root home
129
+ /^\/home\/[^/]+\/\./, // Hidden files in home
130
+ /^C:\\Windows/i, // Windows system
131
+ /^C:\\Program Files/i, // Windows programs
132
+ /\.env$/i, // Environment files
133
+ /credentials\.json$/i, // Credential files
134
+ /\.pem$/i, // Private keys
135
+ /\.key$/i, // Key files
136
+ /id_rsa/i, // SSH keys
137
+ ];
138
+ /**
139
+ * Normalize a path relative to a base path
140
+ *
141
+ * - Resolves relative paths
142
+ * - Prevents path traversal attacks
143
+ * - Returns normalized absolute path
144
+ *
145
+ * @throws McpError if path traversal is detected
146
+ */
147
+ export function normalizePath(inputPath, basePath) {
148
+ const pathApi = getPathApi(basePath);
149
+ // Reject obvious path traversal
150
+ if (inputPath.includes("..")) {
151
+ throw pathTraversalError(inputPath);
152
+ }
153
+ // Resolve the path
154
+ const resolved = pathApi.isAbsolute(inputPath)
155
+ ? inputPath
156
+ : pathApi.resolve(basePath, inputPath);
157
+ // Normalize to remove any . or redundant separators
158
+ const normalized = pathApi.normalize(resolved);
159
+ // Verify the resolved path is within basePath
160
+ const normalizedBase = pathApi.resolve(basePath);
161
+ const relative = pathApi.relative(normalizedBase, normalized);
162
+ if (relative.startsWith("..") || pathApi.isAbsolute(relative)) {
163
+ throw pathTraversalError(inputPath);
164
+ }
165
+ // Check against dangerous patterns
166
+ for (const pattern of DANGEROUS_PATTERNS) {
167
+ if (pattern.test(normalized) || pattern.test(inputPath)) {
168
+ throw pathTraversalError(inputPath);
169
+ }
170
+ }
171
+ return normalized;
172
+ }
173
+ function getPathApi(basePath) {
174
+ // When tests pass POSIX-style base paths on Windows, path.resolve/relative will
175
+ // introduce drive letters (e.g., F:\home\...) and break determinism.
176
+ // Choose the path implementation based on the basePath style.
177
+ if (/^[A-Za-z]:[\\/]/.test(basePath) || basePath.includes("\\")) {
178
+ return path.win32;
179
+ }
180
+ if (basePath.startsWith("/")) {
181
+ return path.posix;
182
+ }
183
+ return path;
184
+ }
185
+ /**
186
+ * Check if a path matches any do_not_touch patterns
187
+ */
188
+ export function matchesDoNotTouch(targetPath, patterns) {
189
+ for (const pattern of patterns) {
190
+ // Try minimatch glob matching
191
+ if (minimatch(targetPath, pattern, { dot: true, matchBase: true })) {
192
+ return pattern;
193
+ }
194
+ // Also check if pattern is a prefix (directory match)
195
+ const normalizedTarget = targetPath.replace(/\\/g, "/");
196
+ const normalizedPattern = pattern.replace(/\\/g, "/");
197
+ if (normalizedTarget.startsWith(normalizedPattern + "/") ||
198
+ normalizedTarget === normalizedPattern) {
199
+ return pattern;
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+ /**
205
+ * Check multiple paths against do_not_touch patterns
206
+ *
207
+ * @returns Object with allowed paths and any violations
208
+ */
209
+ export function checkDoNotTouch(targetPaths, doNotTouchPatterns, basePath) {
210
+ const allowed = [];
211
+ const violations = [];
212
+ const pathApi = getPathApi(basePath);
213
+ for (const targetPath of targetPaths) {
214
+ try {
215
+ const normalized = normalizePath(targetPath, basePath);
216
+ const relativePath = pathApi.relative(basePath, normalized);
217
+ const matchedPattern = matchesDoNotTouch(relativePath, doNotTouchPatterns);
218
+ if (matchedPattern) {
219
+ violations.push({
220
+ path: targetPath,
221
+ pattern: matchedPattern,
222
+ normalizedPath: normalized,
223
+ });
224
+ }
225
+ else {
226
+ allowed.push(normalized);
227
+ }
228
+ }
229
+ catch (e) {
230
+ // Path normalization failed (traversal attack)
231
+ violations.push({
232
+ path: targetPath,
233
+ pattern: "PATH_TRAVERSAL",
234
+ normalizedPath: targetPath,
235
+ });
236
+ }
237
+ }
238
+ return { allowed, violations };
239
+ }
240
+ /**
241
+ * Pre-execution check that throws if any violations are found
242
+ *
243
+ * @throws McpError if any path violates do_not_touch or is a traversal attempt
244
+ */
245
+ export function preExecutionCheck(targetPaths, doNotTouchPatterns, basePath) {
246
+ const { allowed, violations } = checkDoNotTouch(targetPaths, doNotTouchPatterns, basePath);
247
+ if (violations.length > 0) {
248
+ const firstViolation = violations[0];
249
+ if (firstViolation.pattern === "PATH_TRAVERSAL") {
250
+ throw pathTraversalError(firstViolation.path);
251
+ }
252
+ throw doNotTouchError(firstViolation.path, firstViolation.pattern);
253
+ }
254
+ return allowed;
255
+ }
256
+ /**
257
+ * Validate a single path without do_not_touch check (just traversal protection)
258
+ */
259
+ export function validatePath(inputPath, basePath) {
260
+ return normalizePath(inputPath, basePath);
261
+ }
262
+ /**
263
+ * Validate multiple paths and return normalized versions
264
+ */
265
+ export function validatePaths(inputPaths, basePath) {
266
+ return inputPaths.map((p) => normalizePath(p, basePath));
267
+ }
268
+ /**
269
+ * Create path traversal error
270
+ */
271
+ export function pathTraversalError(inputPath) {
272
+ return new McpError(ErrorCode.InvalidParams, `[SECURITY] Path traversal detected: "${inputPath}"`, {
273
+ vibeCode: -32_061,
274
+ category: "SECURITY",
275
+ context: { path: inputPath },
276
+ recovery: "Use paths relative to the project directory without '..' sequences",
277
+ });
278
+ }
279
+ /**
280
+ * Create do_not_touch violation error
281
+ */
282
+ export function doNotTouchError(inputPath, pattern) {
283
+ return new McpError(ErrorCode.InvalidParams, `[SECURITY] Path "${inputPath}" matches do_not_touch pattern "${pattern}"`, {
284
+ vibeCode: -32_063,
285
+ category: "SECURITY",
286
+ context: { path: inputPath, pattern },
287
+ recovery: "This path is protected and cannot be modified. Choose a different target.",
288
+ });
289
+ }