@vibecodetown/mcp-server 2.2.0 → 2.2.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.
Files changed (70) hide show
  1. package/README.md +10 -10
  2. package/build/auth/index.js +0 -2
  3. package/build/auth/public_key.js +6 -4
  4. package/build/bootstrap/doctor.js +113 -5
  5. package/build/bootstrap/installer.js +85 -15
  6. package/build/bootstrap/registry.js +11 -6
  7. package/build/bootstrap/skills-installer.js +365 -0
  8. package/build/dx/activity.js +26 -3
  9. package/build/engine.js +151 -0
  10. package/build/errors.js +107 -0
  11. package/build/generated/bridge_build_seed_input.js +2 -0
  12. package/build/generated/bridge_build_seed_output.js +2 -0
  13. package/build/generated/bridge_confirm_reference_input.js +2 -0
  14. package/build/generated/bridge_confirm_reference_output.js +2 -0
  15. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  16. package/build/generated/bridge_generate_references_input.js +2 -0
  17. package/build/generated/bridge_generate_references_output.js +2 -0
  18. package/build/generated/bridge_references_file.js +2 -0
  19. package/build/generated/bridge_work_order_seed_file.js +2 -0
  20. package/build/generated/contracts_bundle_info.js +3 -3
  21. package/build/generated/index.js +14 -0
  22. package/build/generated/ingress_input.js +2 -0
  23. package/build/generated/ingress_output.js +2 -0
  24. package/build/generated/ingress_resolution_file.js +2 -0
  25. package/build/generated/ingress_summary_file.js +2 -0
  26. package/build/generated/message_template_id_mapping_file.js +2 -0
  27. package/build/generated/run_app_input.js +1 -1
  28. package/build/index.js +4 -3
  29. package/build/local-mode/paths.js +1 -0
  30. package/build/local-mode/setup.js +21 -1
  31. package/build/path-utils.js +68 -0
  32. package/build/runtime/cli_invoker.js +1 -1
  33. package/build/tools/vibe_pm/advisory_review.js +5 -3
  34. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  35. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  36. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  37. package/build/tools/vibe_pm/briefing.js +27 -3
  38. package/build/tools/vibe_pm/context.js +79 -0
  39. package/build/tools/vibe_pm/create_work_order.js +200 -3
  40. package/build/tools/vibe_pm/doctor.js +95 -0
  41. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  42. package/build/tools/vibe_pm/export_output.js +14 -13
  43. package/build/tools/vibe_pm/finalize_work.js +78 -40
  44. package/build/tools/vibe_pm/get_decision.js +2 -2
  45. package/build/tools/vibe_pm/index.js +128 -42
  46. package/build/tools/vibe_pm/ingress.js +645 -0
  47. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  48. package/build/tools/vibe_pm/inspect_code.js +90 -20
  49. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  50. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  51. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  52. package/build/tools/vibe_pm/memory_status.js +11 -8
  53. package/build/tools/vibe_pm/memory_sync.js +11 -8
  54. package/build/tools/vibe_pm/pm_language.js +17 -16
  55. package/build/tools/vibe_pm/python_error.js +115 -0
  56. package/build/tools/vibe_pm/run_app.js +169 -43
  57. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  58. package/build/tools/vibe_pm/search_oss.js +5 -3
  59. package/build/tools/vibe_pm/spec_rag.js +185 -0
  60. package/build/tools/vibe_pm/status.js +50 -3
  61. package/build/tools/vibe_pm/submit_decision.js +2 -2
  62. package/build/tools/vibe_pm/types.js +28 -0
  63. package/build/tools/vibe_pm/undo_last_task.js +9 -2
  64. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  65. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  66. package/build/tools.js +13 -5
  67. package/build/vibe-cli.js +245 -7
  68. package/package.json +5 -4
  69. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  70. package/skills/index.json +14 -0
@@ -0,0 +1,115 @@
1
+ // adapters/mcp-ts/src/tools/vibe_pm/python_error.ts
2
+ // Shared Python CLI error classification
3
+ /**
4
+ * Classify Python CLI error for better diagnostics
5
+ *
6
+ * @param stderr - Standard error output from Python CLI
7
+ * @param code - Exit code
8
+ * @param context - Optional context (e.g., "Local Memory", "Advisory Review")
9
+ * @returns Classified error with actionable next steps
10
+ */
11
+ export function classifyPythonError(stderr, code, context) {
12
+ const lowerStderr = stderr.toLowerCase();
13
+ const ctx = context ?? "작업";
14
+ // ModuleNotFoundError: vibecoding_helper not found
15
+ if (lowerStderr.includes("modulenotfounderror") || lowerStderr.includes("no module named")) {
16
+ return {
17
+ issueCode: "python_module_not_found",
18
+ summary: "Python 패키지를 찾을 수 없습니다. PYTHONPATH 또는 패키지 설치를 확인하세요.",
19
+ nextAction: { tool: "vibe_pm.doctor", reason: "Python 환경을 점검하겠습니다." }
20
+ };
21
+ }
22
+ // Rust binary error (wrong routing - should not happen after fix)
23
+ if (lowerStderr.includes("unrecognized subcommand") || lowerStderr.includes("usage: vibecoding-helper")) {
24
+ return {
25
+ issueCode: "wrong_binary_routing",
26
+ summary: "Python CLI 대신 Rust 바이너리가 호출되었습니다. MCP 서버 버전을 확인하세요.",
27
+ nextAction: { tool: "vibe_pm.update", reason: "MCP 서버를 최신 버전으로 업데이트하겠습니다." }
28
+ };
29
+ }
30
+ // Python not found
31
+ if (lowerStderr.includes("python") && (lowerStderr.includes("not found") || lowerStderr.includes("command not found"))) {
32
+ return {
33
+ issueCode: "python_not_found",
34
+ summary: "Python이 설치되어 있지 않거나 PATH에 없습니다.",
35
+ nextAction: { tool: "vibe_pm.doctor", reason: "Python 설치 상태를 점검하겠습니다." }
36
+ };
37
+ }
38
+ // ChromaDB / embedding error
39
+ if (lowerStderr.includes("chromadb") || lowerStderr.includes("embedding")) {
40
+ return {
41
+ issueCode: "chromadb_error",
42
+ summary: "Local Memory 인덱스(ChromaDB) 오류가 발생했습니다.",
43
+ nextAction: { tool: "vibe_pm.memory_sync", reason: "인덱스를 재생성하겠습니다." }
44
+ };
45
+ }
46
+ // Permission error
47
+ if (lowerStderr.includes("permission denied") || lowerStderr.includes("access denied")) {
48
+ return {
49
+ issueCode: "permission_denied",
50
+ summary: "파일 접근 권한 오류입니다.",
51
+ nextAction: { tool: "vibe_pm.doctor", reason: "파일 권한을 점검하겠습니다." }
52
+ };
53
+ }
54
+ // Disk space / write error
55
+ if (lowerStderr.includes("no space left") || lowerStderr.includes("disk full")) {
56
+ return {
57
+ issueCode: "disk_full",
58
+ summary: "디스크 공간이 부족합니다.",
59
+ nextAction: { tool: "vibe_pm.doctor", reason: "디스크 상태를 점검하겠습니다." }
60
+ };
61
+ }
62
+ // Timeout
63
+ if (lowerStderr.includes("[timeout]")) {
64
+ return {
65
+ issueCode: "timeout",
66
+ summary: `${ctx} 실행이 시간 초과되었습니다.`,
67
+ nextAction: { tool: "vibe_pm.doctor", reason: "시스템 상태를 점검하겠습니다." }
68
+ };
69
+ }
70
+ // Import error (other than module not found)
71
+ if (lowerStderr.includes("importerror")) {
72
+ return {
73
+ issueCode: "import_error",
74
+ summary: "Python 모듈 import 중 오류가 발생했습니다. 의존성을 확인하세요.",
75
+ nextAction: { tool: "vibe_pm.doctor", reason: "Python 환경을 점검하겠습니다." }
76
+ };
77
+ }
78
+ // Syntax error
79
+ if (lowerStderr.includes("syntaxerror")) {
80
+ return {
81
+ issueCode: "syntax_error",
82
+ summary: "Python 코드에 문법 오류가 있습니다. 패키지 버전을 확인하세요.",
83
+ nextAction: { tool: "vibe_pm.update", reason: "패키지를 최신 버전으로 업데이트하겠습니다." }
84
+ };
85
+ }
86
+ // JSON decode error
87
+ if (lowerStderr.includes("jsondecodeerror") || lowerStderr.includes("json.decoder")) {
88
+ return {
89
+ issueCode: "json_decode_error",
90
+ summary: "JSON 파싱 오류가 발생했습니다. 입력 데이터를 확인하세요.",
91
+ nextAction: { tool: "vibe_pm.status", reason: "현재 상태를 확인하겠습니다." }
92
+ };
93
+ }
94
+ // Network error
95
+ if (lowerStderr.includes("connectionerror") || lowerStderr.includes("urlerror") || lowerStderr.includes("timeout") && lowerStderr.includes("connect")) {
96
+ return {
97
+ issueCode: "network_error",
98
+ summary: "네트워크 연결 오류가 발생했습니다.",
99
+ nextAction: { tool: "vibe_pm.doctor", reason: "네트워크 상태를 점검하겠습니다." }
100
+ };
101
+ }
102
+ // Default: unknown error
103
+ return {
104
+ issueCode: `engine_exit_code_${code}`,
105
+ summary: `${ctx}에 실패했습니다.`,
106
+ nextAction: { tool: "vibe_pm.doctor", reason: "설치/환경 상태를 점검하겠습니다." }
107
+ };
108
+ }
109
+ /**
110
+ * Format error issue string
111
+ */
112
+ export function formatErrorIssue(classified, stderr, code) {
113
+ const detail = stderr?.trim() || `exit_code=${code}`;
114
+ return `${classified.issueCode}: ${detail}`;
115
+ }
@@ -5,9 +5,12 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { createHash } from "node:crypto";
8
+ import { decode as decodeMsgpack } from "@msgpack/msgpack";
8
9
  import { invokeSystem, invokeSystemSync, spawnDetached } from "../../runtime/cli_invoker.js";
9
- import { resolveRunDir, resolveRunId } from "./context.js";
10
+ import { getRunContext, resolveRunDir, resolveRunId } from "./context.js";
10
11
  import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, normalizePorts } from "./run_app_podman.js";
12
+ import { isOpaEvalFailedError, isOpaMissingError } from "../../errors.js";
13
+ import { toPosixPath, resolveRelativePosix, normalizePathArray } from "../../path-utils.js";
11
14
  /**
12
15
  * vibe_pm.run_app - Auto-detect project type and run
13
16
  *
@@ -17,21 +20,28 @@ import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, no
17
20
  * 3. Start process in background
18
21
  * 4. Return URL and PID
19
22
  */
20
- export async function runApp(input) {
21
- const basePath = process.cwd();
23
+ export async function runApp(input, basePath = process.cwd()) {
22
24
  const mode = input.mode ?? "dev";
23
25
  const customPort = input.port;
24
26
  const container = input.container ?? "none";
25
- // Step 1: Detect project type and get run info
26
- const projectInfo = detectProject(basePath, mode, customPort);
27
- if (projectInfo.type === "unknown") {
27
+ const runtime = container === "podman" ? "podman" : "host";
28
+ // Step 1: Resolve execution candidate from ingress entrypoints artifact (SSOT)
29
+ const runIdForArtifacts = resolveRunIdForSecurity(basePath);
30
+ const entrySel = selectEntrypointCandidate({
31
+ basePath,
32
+ run_id: runIdForArtifacts,
33
+ mode,
34
+ option_key: input?.option_key
35
+ });
36
+ if (!entrySel.ok) {
28
37
  return {
29
38
  success: false,
30
- command_executed: "detect_project",
31
- message: "프로젝트 유형을 감지할 수 없습니다. package.json 또는 requirements.txt가 필요합니다.",
32
- runtime: "host"
39
+ command_executed: "select_entrypoint",
40
+ message: entrySel.message,
41
+ runtime
33
42
  };
34
43
  }
44
+ const projectInfo = projectInfoFromCandidate(entrySel.candidate, mode, customPort);
35
45
  if (container === "podman") {
36
46
  return await runAppWithPodman(input, basePath, mode, projectInfo);
37
47
  }
@@ -45,7 +55,7 @@ export async function runApp(input) {
45
55
  success: false,
46
56
  command_executed: `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim(),
47
57
  message: formatOpaBlockMessage(opaDecision),
48
- runtime: "host"
58
+ runtime
49
59
  };
50
60
  }
51
61
  // Step 2: Start process
@@ -63,7 +73,9 @@ export async function runApp(input) {
63
73
  port: projectInfo.port,
64
74
  url: projectInfo.url,
65
75
  command_executed: fullCommand,
66
- process_id: pid ?? undefined
76
+ process_id: pid ?? undefined,
77
+ entrypoint_key: entrySel.candidate.key,
78
+ entrypoint_label: entrySel.candidate.label
67
79
  });
68
80
  return {
69
81
  success: true,
@@ -71,7 +83,7 @@ export async function runApp(input) {
71
83
  url: projectInfo.url,
72
84
  command_executed: fullCommand,
73
85
  message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
74
- runtime: "host"
86
+ runtime
75
87
  };
76
88
  }
77
89
  catch (err) {
@@ -79,20 +91,132 @@ export async function runApp(input) {
79
91
  success: false,
80
92
  command_executed: fullCommand,
81
93
  message: `실행 실패: ${err instanceof Error ? err.message : String(err)}`,
82
- runtime: "host"
94
+ runtime
95
+ };
96
+ }
97
+ }
98
+ function selectEntrypointCandidate(args) {
99
+ const context = getRunContext(args.run_id, args.basePath);
100
+ const entrypointsAbs = path.join(context.runs_path, "ingress", "entrypoints.msgpack");
101
+ if (!fs.existsSync(entrypointsAbs)) {
102
+ return {
103
+ ok: false,
104
+ message: "실행 후보 정보를 찾을 수 없습니다. 먼저 프로젝트 시작(ingress) 단계에서 사전 점검을 완료해주세요."
105
+ };
106
+ }
107
+ let decoded;
108
+ try {
109
+ const bytes = fs.readFileSync(entrypointsAbs);
110
+ decoded = decodeMsgpack(bytes);
111
+ }
112
+ catch {
113
+ return {
114
+ ok: false,
115
+ message: "실행 후보 정보를 읽을 수 없습니다. 사전 점검을 다시 실행해 주세요."
116
+ };
117
+ }
118
+ const doc = decoded;
119
+ const rawCandidates = args.mode === "test" ? doc.test_candidates : doc.run_candidates;
120
+ const candidates = normalizeEntrypointCandidates(rawCandidates);
121
+ if (candidates.length === 0) {
122
+ return {
123
+ ok: false,
124
+ message: "실행 후보가 없습니다. 사전 점검 결과를 확인하거나 실행 방법을 먼저 정리해 주세요."
83
125
  };
84
126
  }
127
+ const optionKey = typeof args.option_key === "string" ? args.option_key.trim() : "";
128
+ if (candidates.length >= 2) {
129
+ if (!["A", "B", "C"].includes(optionKey)) {
130
+ const choices = candidates
131
+ .slice(0, 3)
132
+ .map((c) => `${c.key}: ${c.label}`)
133
+ .join(" / ");
134
+ return {
135
+ ok: false,
136
+ message: `실행 후보가 여러 개입니다. option_key(A/B/C)로 하나를 선택해 주세요. (${choices})`
137
+ };
138
+ }
139
+ const picked = candidates.find((c) => c.key === optionKey);
140
+ if (!picked) {
141
+ return {
142
+ ok: false,
143
+ message: "선택한 실행 후보를 찾을 수 없습니다. A/B/C 중 다시 선택해 주세요."
144
+ };
145
+ }
146
+ return { ok: true, candidate: picked };
147
+ }
148
+ return { ok: true, candidate: candidates[0] };
149
+ }
150
+ function normalizeEntrypointCandidates(raw) {
151
+ if (!Array.isArray(raw))
152
+ return [];
153
+ const out = [];
154
+ for (const item of raw) {
155
+ const obj = item;
156
+ const key = typeof obj?.key === "string" ? obj.key.trim() : "";
157
+ const label = typeof obj?.label === "string" ? obj.label.trim() : "";
158
+ const cmdArr = Array.isArray(obj?.command) ? obj.command.filter((x) => typeof x === "string") : [];
159
+ const command = cmdArr.map((x) => String(x).trim()).filter(Boolean);
160
+ if (!["A", "B", "C"].includes(key))
161
+ continue;
162
+ if (!label)
163
+ continue;
164
+ if (command.length === 0)
165
+ continue;
166
+ out.push({
167
+ key: key,
168
+ label,
169
+ kind: typeof obj?.kind === "string" ? obj.kind : undefined,
170
+ confidence: typeof obj?.confidence === "number" ? obj.confidence : undefined,
171
+ cwd: typeof obj?.cwd === "string" ? obj.cwd : undefined,
172
+ command,
173
+ evidence: Array.isArray(obj?.evidence) ? obj.evidence.filter((x) => typeof x === "string") : undefined
174
+ });
175
+ }
176
+ // Deterministic ordering: key order A/B/C
177
+ const order = { A: 1, B: 2, C: 3 };
178
+ return out.sort((a, b) => order[a.key] - order[b.key]);
179
+ }
180
+ function projectInfoFromCandidate(candidate, mode, customPort) {
181
+ const cmd = candidate.command[0] ?? "";
182
+ const args = candidate.command.slice(1);
183
+ const type = deriveProjectType(cmd);
184
+ const portDefault = type === "python" ? 8000 : 3000;
185
+ const port = customPort ?? portDefault;
186
+ const url = `http://localhost:${port}`;
187
+ // In v1: enforce repo-root execution for policy simplicity.
188
+ if (candidate.cwd && candidate.cwd !== ".") {
189
+ // Keep it deterministic and safe (monorepo support can be added later with explicit policy updates).
190
+ // Do not throw; return a safe default that will fail OPA or execution if misused.
191
+ }
192
+ // If mode=test and the candidate is a test runner, still keep url/port stable for output shape.
193
+ return { type, command: cmd, args, port, url };
194
+ }
195
+ function deriveProjectType(command) {
196
+ const c = (command ?? "").trim().toLowerCase();
197
+ if (!c)
198
+ return "unknown";
199
+ if (["npm", "pnpm", "yarn", "bun", "node", "npx"].includes(c))
200
+ return "node";
201
+ if (["python", "python3", "uvicorn"].includes(c))
202
+ return "python";
203
+ return "unknown";
85
204
  }
86
205
  function stableShortHash(input) {
87
206
  return createHash("sha256").update(input).digest("hex").slice(0, 12);
88
207
  }
208
+ function isCurrentWorkOrder(value) {
209
+ return typeof value === "object" && value !== null;
210
+ }
89
211
  function readCurrentWorkOrderRunId(basePath) {
90
212
  const workOrderPath = path.join(basePath, ".vibe", "state", "current_work_order.json");
91
213
  if (!fs.existsSync(workOrderPath))
92
214
  return null;
93
215
  try {
94
216
  const raw = JSON.parse(fs.readFileSync(workOrderPath, "utf-8"));
95
- const runId = typeof raw?.run_id === "string" ? raw.run_id.trim() : "";
217
+ if (!isCurrentWorkOrder(raw))
218
+ return null;
219
+ const runId = typeof raw.run_id === "string" ? raw.run_id.trim() : "";
96
220
  if (!runId)
97
221
  return null;
98
222
  if (runId.includes("..") || runId.includes("/") || runId.includes("\\") || runId.includes("\0")) {
@@ -128,31 +252,11 @@ function writeJsonBestEffort(filePath, payload) {
128
252
  // ignore
129
253
  }
130
254
  }
131
- function normalizeOpaPath(value) {
132
- return value.replace(/\\/g, "/");
133
- }
134
- function normalizeOpaPathRelative(basePath, value) {
135
- const trimmed = value.trim();
136
- if (!trimmed)
137
- return "";
138
- const absolute = path.isAbsolute(trimmed) ? trimmed : path.resolve(basePath, trimmed);
139
- const relative = path.relative(basePath, absolute);
140
- if (!relative)
141
- return ".";
142
- return normalizeOpaPath(relative);
143
- }
144
- function normalizeOpaPaths(basePath, values) {
145
- const seen = new Set();
146
- const out = [];
147
- for (const raw of values) {
148
- const normalized = normalizeOpaPathRelative(basePath, raw);
149
- if (!normalized || seen.has(normalized))
150
- continue;
151
- seen.add(normalized);
152
- out.push(normalized);
153
- }
154
- return out;
155
- }
255
+ // P1-5: Path normalization functions consolidated into path-utils.ts
256
+ // Aliases for backward compatibility within this file
257
+ const normalizeOpaPath = toPosixPath;
258
+ const normalizeOpaPathRelative = resolveRelativePosix;
259
+ const normalizeOpaPaths = normalizePathArray;
156
260
  /**
157
261
  * P1-3: Extract script name from package manager commands
158
262
  * npm run dev -> "dev", npm start -> "start", etc.
@@ -195,6 +299,26 @@ function buildOpaPolicyInput(basePath, command, args, opts) {
195
299
  }
196
300
  };
197
301
  }
302
+ /**
303
+ * P1-6: Resolve OPA policy path with environment variable override support.
304
+ * VIBECODE_OPA_POLICY_PATH - Override default policy file path
305
+ * VIBECODE_OPA_POLICY_DIR - Override policy directory (appends /vibepm/run_app.rego)
306
+ */
307
+ function resolveOpaPolicyPath(basePath) {
308
+ // Direct path override (highest priority)
309
+ const envPath = process.env.VIBECODE_OPA_POLICY_PATH;
310
+ if (envPath) {
311
+ return path.isAbsolute(envPath) ? envPath : path.resolve(basePath, envPath);
312
+ }
313
+ // Directory override (middle priority)
314
+ const envDir = process.env.VIBECODE_OPA_POLICY_DIR;
315
+ if (envDir) {
316
+ const baseDir = path.isAbsolute(envDir) ? envDir : path.resolve(basePath, envDir);
317
+ return path.join(baseDir, "vibepm", "run_app.rego");
318
+ }
319
+ // Default path
320
+ return path.join(basePath, "policy", "vibepm", "run_app.rego");
321
+ }
198
322
  function evaluateOpaGate(basePath, input) {
199
323
  const runId = resolveRunIdForSecurity(basePath);
200
324
  const securityDir = resolveSecurityDir(basePath, runId);
@@ -202,7 +326,8 @@ function evaluateOpaGate(basePath, input) {
202
326
  const resultPath = securityDir ? path.join(securityDir, "policy_result.json") : null;
203
327
  if (inputPath)
204
328
  writeJsonBestEffort(inputPath, input);
205
- const policyPath = path.join(basePath, "policy", "vibepm", "run_app.rego");
329
+ // P1-6: Support environment variable override for policy path
330
+ const policyPath = resolveOpaPolicyPath(basePath);
206
331
  if (!fs.existsSync(policyPath)) {
207
332
  const deny = {
208
333
  allow: false,
@@ -271,11 +396,12 @@ function evaluateOpaGate(basePath, input) {
271
396
  }
272
397
  }
273
398
  function formatOpaBlockMessage(result) {
274
- const text = `${result.reasons?.join(" ") ?? ""} ${(result.matched_rules ?? []).join(" ")}`.toLowerCase();
275
- if (text.includes("missing")) {
399
+ // P1-4: Use centralized error patterns
400
+ const text = `${result.reasons?.join(" ") ?? ""} ${(result.matched_rules ?? []).join(" ")}`;
401
+ if (isOpaMissingError(text)) {
276
402
  return "OPA가 설치되어 있지 않거나 정책 파일이 없어 실행을 차단했습니다. OPA 설치/정책 파일을 확인하세요.";
277
403
  }
278
- if (text.includes("eval_failed")) {
404
+ if (isOpaEvalFailedError(text)) {
279
405
  return "OPA 평가에 실패하여 안전을 위해 실행을 차단했습니다. OPA 상태를 확인하세요.";
280
406
  }
281
407
  return "OPA 정책에서 허용되지 않은 커맨드/경로입니다. 정책을 만족하도록 수정하세요.";
@@ -1,14 +1,76 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/run_app_podman.ts
2
2
  // Pure helpers for Podman run_app path (testable without spawning Podman)
3
3
  import * as path from "node:path";
4
- export const CONTAINER_ENV_ALLOWLIST = new Set(["NODE_ENV", "PORT", "PYTHONUNBUFFERED", "CI"]);
4
+ // P2-22: Environment variable filtering for container security
5
+ // Allowlist: Safe environment variables that can be passed to containers
6
+ export const CONTAINER_ENV_ALLOWLIST = new Set([
7
+ // Node.js
8
+ "NODE_ENV",
9
+ "PORT",
10
+ "HOST",
11
+ // Python
12
+ "PYTHONUNBUFFERED",
13
+ "PYTHONDONTWRITEBYTECODE",
14
+ // General
15
+ "CI",
16
+ "TZ",
17
+ "LANG",
18
+ "LC_ALL",
19
+ // Development
20
+ "DEBUG",
21
+ "LOG_LEVEL",
22
+ "VERBOSE",
23
+ ]);
24
+ // Blocklist: Sensitive environment variables that must NEVER be passed
25
+ const CONTAINER_ENV_BLOCKLIST = new Set([
26
+ // Secrets and credentials
27
+ "AWS_ACCESS_KEY_ID",
28
+ "AWS_SECRET_ACCESS_KEY",
29
+ "AWS_SESSION_TOKEN",
30
+ "GITHUB_TOKEN",
31
+ "GH_TOKEN",
32
+ "NPM_TOKEN",
33
+ "DOCKER_PASSWORD",
34
+ "DATABASE_URL",
35
+ "DB_PASSWORD",
36
+ "API_KEY",
37
+ "SECRET_KEY",
38
+ "PRIVATE_KEY",
39
+ "PASSWORD",
40
+ // Cloud provider credentials
41
+ "GOOGLE_APPLICATION_CREDENTIALS",
42
+ "AZURE_CLIENT_SECRET",
43
+ // SSH and authentication
44
+ "SSH_AUTH_SOCK",
45
+ "SSH_AGENT_PID",
46
+ ]);
47
+ // Patterns for sensitive variable names (regex)
48
+ const SENSITIVE_PATTERNS = [
49
+ /SECRET/i,
50
+ /PASSWORD/i,
51
+ /TOKEN$/i,
52
+ /API_KEY$/i,
53
+ /PRIVATE_KEY/i,
54
+ /CREDENTIAL/i,
55
+ ];
56
+ function isSensitiveEnvVar(name) {
57
+ if (CONTAINER_ENV_BLOCKLIST.has(name))
58
+ return true;
59
+ return SENSITIVE_PATTERNS.some((pattern) => pattern.test(name));
60
+ }
5
61
  export function filterContainerEnvAllowlist(env) {
6
62
  const out = {};
7
63
  if (!env)
8
64
  return out;
9
65
  for (const [k, v] of Object.entries(env)) {
10
- if (CONTAINER_ENV_ALLOWLIST.has(k))
66
+ // P2-22: Block sensitive variables even if explicitly provided
67
+ if (isSensitiveEnvVar(k)) {
68
+ console.warn(`[run_app] Blocked sensitive env var: ${k}`);
69
+ continue;
70
+ }
71
+ if (CONTAINER_ENV_ALLOWLIST.has(k)) {
11
72
  out[k] = v;
73
+ }
12
74
  }
13
75
  return out;
14
76
  }
@@ -1,10 +1,11 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/search_oss.ts
2
2
  // vibe_pm.search_oss - Search OSS repos (gh) and write evidence under runs/<run_id>/export/
3
3
  import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
4
- import { runEngine } from "../../engine.js";
4
+ import { runPythonCli } from "../../engine.js";
5
5
  import { safeJsonParse } from "../../cli.js";
6
6
  import { validateToolInput } from "../../security/input-validator.js";
7
7
  import { resolveProjectId, resolveRunId } from "./context.js";
8
+ import { classifyPythonError } from "./python_error.js";
8
9
  function toCsv(values) {
9
10
  return values
10
11
  .map((v) => (typeof v === "string" ? v.trim() : ""))
@@ -64,9 +65,10 @@ export async function searchOss(input) {
64
65
  const writeEvidence = input?.write_evidence !== false;
65
66
  if (!writeEvidence)
66
67
  cmd.push("--no-write-evidence");
67
- const res = await runEngine("vibecoding-helper", cmd, { timeoutMs: 180_000 });
68
+ const res = await runPythonCli(cmd, { timeoutMs: 180_000 });
68
69
  if (res.code !== 0) {
69
- throw new Error(`search-oss failed: ${res.stderr || res.stdout || `exit_code=${res.code}`}`);
70
+ const classified = classifyPythonError(res.stderr ?? "", res.code, "OSS 검색");
71
+ throw new Error(`${classified.issueCode}: ${classified.summary}`);
70
72
  }
71
73
  const parsed = safeJsonParse(res.stdout);
72
74
  if (!parsed.ok) {