@vibecodetown/mcp-server 2.1.4 → 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 (80) hide show
  1. package/README.md +10 -10
  2. package/build/auth/credential_store.js +146 -0
  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/control_plane/gate.js +52 -70
  9. package/build/dx/activity.js +26 -3
  10. package/build/engine.js +151 -0
  11. package/build/errors.js +107 -0
  12. package/build/generated/bridge_build_seed_input.js +2 -0
  13. package/build/generated/bridge_build_seed_output.js +2 -0
  14. package/build/generated/bridge_confirm_reference_input.js +2 -0
  15. package/build/generated/bridge_confirm_reference_output.js +2 -0
  16. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  17. package/build/generated/bridge_generate_references_input.js +2 -0
  18. package/build/generated/bridge_generate_references_output.js +2 -0
  19. package/build/generated/bridge_references_file.js +2 -0
  20. package/build/generated/bridge_work_order_seed_file.js +2 -0
  21. package/build/generated/contracts_bundle_info.js +3 -3
  22. package/build/generated/index.js +14 -0
  23. package/build/generated/ingress_input.js +2 -0
  24. package/build/generated/ingress_output.js +2 -0
  25. package/build/generated/ingress_resolution_file.js +2 -0
  26. package/build/generated/ingress_summary_file.js +2 -0
  27. package/build/generated/message_template_id_mapping_file.js +2 -0
  28. package/build/generated/run_app_input.js +1 -1
  29. package/build/index.js +4 -1
  30. package/build/local-mode/git.js +36 -22
  31. package/build/local-mode/paths.js +1 -0
  32. package/build/local-mode/project-state.js +176 -0
  33. package/build/local-mode/setup.js +21 -1
  34. package/build/local-mode/templates.js +3 -3
  35. package/build/path-utils.js +68 -0
  36. package/build/runtime/cli_invoker.js +416 -0
  37. package/build/tools/vibe_pm/advisory_review.js +5 -3
  38. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  39. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  40. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  41. package/build/tools/vibe_pm/briefing.js +26 -1
  42. package/build/tools/vibe_pm/context.js +79 -0
  43. package/build/tools/vibe_pm/create_work_order.js +200 -3
  44. package/build/tools/vibe_pm/doctor.js +95 -0
  45. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  46. package/build/tools/vibe_pm/export_output.js +14 -13
  47. package/build/tools/vibe_pm/finalize_work.js +74 -0
  48. package/build/tools/vibe_pm/force_override.js +104 -0
  49. package/build/tools/vibe_pm/get_decision.js +2 -2
  50. package/build/tools/vibe_pm/index.js +160 -3
  51. package/build/tools/vibe_pm/ingress.js +645 -0
  52. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  53. package/build/tools/vibe_pm/inspect_code.js +90 -20
  54. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  55. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  56. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  57. package/build/tools/vibe_pm/list_rules.js +135 -0
  58. package/build/tools/vibe_pm/memory_status.js +11 -8
  59. package/build/tools/vibe_pm/memory_sync.js +11 -8
  60. package/build/tools/vibe_pm/pm_language.js +17 -16
  61. package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
  62. package/build/tools/vibe_pm/publish_mcp.js +271 -0
  63. package/build/tools/vibe_pm/python_error.js +115 -0
  64. package/build/tools/vibe_pm/run_app.js +215 -86
  65. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  66. package/build/tools/vibe_pm/save_rule.js +120 -0
  67. package/build/tools/vibe_pm/search_oss.js +5 -3
  68. package/build/tools/vibe_pm/spec_rag.js +185 -0
  69. package/build/tools/vibe_pm/status.js +50 -3
  70. package/build/tools/vibe_pm/submit_decision.js +2 -2
  71. package/build/tools/vibe_pm/types.js +28 -0
  72. package/build/tools/vibe_pm/undo_last_task.js +23 -20
  73. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  74. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  75. package/build/tools.js +13 -5
  76. package/build/version-check.js +5 -5
  77. package/build/vibe-cli.js +742 -39
  78. package/package.json +5 -4
  79. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  80. 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
+ }
@@ -1,11 +1,16 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/run_app.ts
2
2
  // vibe_pm.run_app - One-click application launch
3
+ //
4
+ // All subprocess invocations go through cli_invoker for centralized management.
3
5
  import * as fs from "node:fs";
4
6
  import * as path from "node:path";
5
- import { spawn, spawnSync } from "node:child_process";
6
7
  import { createHash } from "node:crypto";
7
- import { resolveRunDir, resolveRunId } from "./context.js";
8
+ import { decode as decodeMsgpack } from "@msgpack/msgpack";
9
+ import { invokeSystem, invokeSystemSync, spawnDetached } from "../../runtime/cli_invoker.js";
10
+ import { getRunContext, resolveRunDir, resolveRunId } from "./context.js";
8
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";
9
14
  /**
10
15
  * vibe_pm.run_app - Auto-detect project type and run
11
16
  *
@@ -15,21 +20,28 @@ import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, no
15
20
  * 3. Start process in background
16
21
  * 4. Return URL and PID
17
22
  */
18
- export async function runApp(input) {
19
- const basePath = process.cwd();
23
+ export async function runApp(input, basePath = process.cwd()) {
20
24
  const mode = input.mode ?? "dev";
21
25
  const customPort = input.port;
22
26
  const container = input.container ?? "none";
23
- // Step 1: Detect project type and get run info
24
- const projectInfo = detectProject(basePath, mode, customPort);
25
- 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) {
26
37
  return {
27
38
  success: false,
28
- command_executed: "detect_project",
29
- message: "프로젝트 유형을 감지할 수 없습니다. package.json 또는 requirements.txt가 필요합니다.",
30
- runtime: "host"
39
+ command_executed: "select_entrypoint",
40
+ message: entrySel.message,
41
+ runtime
31
42
  };
32
43
  }
44
+ const projectInfo = projectInfoFromCandidate(entrySel.candidate, mode, customPort);
33
45
  if (container === "podman") {
34
46
  return await runAppWithPodman(input, basePath, mode, projectInfo);
35
47
  }
@@ -43,25 +55,16 @@ export async function runApp(input) {
43
55
  success: false,
44
56
  command_executed: `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim(),
45
57
  message: formatOpaBlockMessage(opaDecision),
46
- runtime: "host"
58
+ runtime
47
59
  };
48
60
  }
49
61
  // Step 2: Start process
50
62
  const fullCommand = `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim();
51
63
  try {
52
- const child = spawn(projectInfo.command, projectInfo.args, {
53
- cwd: basePath,
54
- detached: true,
55
- stdio: "ignore",
56
- env: {
57
- ...process.env,
58
- PORT: String(projectInfo.port),
59
- NODE_ENV: mode === "prod" ? "production" : "development"
60
- }
64
+ const pid = spawnDetached(projectInfo.command, projectInfo.args, basePath, {
65
+ PORT: String(projectInfo.port),
66
+ NODE_ENV: mode === "prod" ? "production" : "development"
61
67
  });
62
- // Unref to allow parent to exit independently
63
- child.unref();
64
- const pid = child.pid;
65
68
  // Give the process a moment to start
66
69
  await new Promise((resolve) => setTimeout(resolve, 500));
67
70
  writeRunAppEvidenceBestEffort(basePath, {
@@ -70,15 +73,17 @@ export async function runApp(input) {
70
73
  port: projectInfo.port,
71
74
  url: projectInfo.url,
72
75
  command_executed: fullCommand,
73
- process_id: pid
76
+ process_id: pid ?? undefined,
77
+ entrypoint_key: entrySel.candidate.key,
78
+ entrypoint_label: entrySel.candidate.label
74
79
  });
75
80
  return {
76
81
  success: true,
77
- process_id: pid,
82
+ process_id: pid ?? undefined,
78
83
  url: projectInfo.url,
79
84
  command_executed: fullCommand,
80
85
  message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
81
- runtime: "host"
86
+ runtime
82
87
  };
83
88
  }
84
89
  catch (err) {
@@ -86,20 +91,132 @@ export async function runApp(input) {
86
91
  success: false,
87
92
  command_executed: fullCommand,
88
93
  message: `실행 실패: ${err instanceof Error ? err.message : String(err)}`,
89
- 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: "실행 후보 정보를 읽을 수 없습니다. 사전 점검을 다시 실행해 주세요."
90
116
  };
91
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: "실행 후보가 없습니다. 사전 점검 결과를 확인하거나 실행 방법을 먼저 정리해 주세요."
125
+ };
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";
92
204
  }
93
205
  function stableShortHash(input) {
94
206
  return createHash("sha256").update(input).digest("hex").slice(0, 12);
95
207
  }
208
+ function isCurrentWorkOrder(value) {
209
+ return typeof value === "object" && value !== null;
210
+ }
96
211
  function readCurrentWorkOrderRunId(basePath) {
97
212
  const workOrderPath = path.join(basePath, ".vibe", "state", "current_work_order.json");
98
213
  if (!fs.existsSync(workOrderPath))
99
214
  return null;
100
215
  try {
101
216
  const raw = JSON.parse(fs.readFileSync(workOrderPath, "utf-8"));
102
- 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() : "";
103
220
  if (!runId)
104
221
  return null;
105
222
  if (runId.includes("..") || runId.includes("/") || runId.includes("\\") || runId.includes("\0")) {
@@ -135,35 +252,36 @@ function writeJsonBestEffort(filePath, payload) {
135
252
  // ignore
136
253
  }
137
254
  }
138
- function normalizeOpaPath(value) {
139
- return value.replace(/\\/g, "/");
140
- }
141
- function normalizeOpaPathRelative(basePath, value) {
142
- const trimmed = value.trim();
143
- if (!trimmed)
144
- return "";
145
- const absolute = path.isAbsolute(trimmed) ? trimmed : path.resolve(basePath, trimmed);
146
- const relative = path.relative(basePath, absolute);
147
- if (!relative)
148
- return ".";
149
- return normalizeOpaPath(relative);
150
- }
151
- function normalizeOpaPaths(basePath, values) {
152
- const seen = new Set();
153
- const out = [];
154
- for (const raw of values) {
155
- const normalized = normalizeOpaPathRelative(basePath, raw);
156
- if (!normalized || seen.has(normalized))
157
- continue;
158
- seen.add(normalized);
159
- out.push(normalized);
160
- }
161
- return out;
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;
260
+ /**
261
+ * P1-3: Extract script name from package manager commands
262
+ * npm run dev -> "dev", npm start -> "start", etc.
263
+ */
264
+ function extractScriptName(command, args) {
265
+ const packageManagers = ["npm", "yarn", "pnpm", "bun"];
266
+ if (!packageManagers.includes(command))
267
+ return undefined;
268
+ // npm run <script>, yarn run <script>, pnpm run <script>, bun run <script>
269
+ const runIdx = args.indexOf("run");
270
+ if (runIdx >= 0 && args[runIdx + 1]) {
271
+ return args[runIdx + 1];
272
+ }
273
+ // npm start, npm test, etc. (shortcuts)
274
+ const shortcuts = ["start", "test", "build"];
275
+ if (args.length > 0 && shortcuts.includes(args[0])) {
276
+ return args[0];
277
+ }
278
+ return undefined;
162
279
  }
163
280
  function buildOpaPolicyInput(basePath, command, args, opts) {
164
281
  const cwd = normalizeOpaPath(basePath);
165
282
  const paths = normalizeOpaPaths(basePath, opts?.paths ?? [basePath]);
166
283
  const mounts = opts?.mounts ? normalizeOpaPaths(basePath, opts.mounts) : undefined;
284
+ const scriptName = extractScriptName(command, args);
167
285
  return {
168
286
  action: "run_app",
169
287
  project_root: cwd,
@@ -172,7 +290,8 @@ function buildOpaPolicyInput(basePath, command, args, opts) {
172
290
  args,
173
291
  cwd,
174
292
  paths,
175
- mounts
293
+ mounts,
294
+ script_name: scriptName
176
295
  },
177
296
  meta: {
178
297
  tool: "vibe_pm.run_app",
@@ -180,6 +299,26 @@ function buildOpaPolicyInput(basePath, command, args, opts) {
180
299
  }
181
300
  };
182
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
+ }
183
322
  function evaluateOpaGate(basePath, input) {
184
323
  const runId = resolveRunIdForSecurity(basePath);
185
324
  const securityDir = resolveSecurityDir(basePath, runId);
@@ -187,7 +326,8 @@ function evaluateOpaGate(basePath, input) {
187
326
  const resultPath = securityDir ? path.join(securityDir, "policy_result.json") : null;
188
327
  if (inputPath)
189
328
  writeJsonBestEffort(inputPath, input);
190
- 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);
191
331
  if (!fs.existsSync(policyPath)) {
192
332
  const deny = {
193
333
  allow: false,
@@ -198,14 +338,14 @@ function evaluateOpaGate(basePath, input) {
198
338
  writeJsonBestEffort(resultPath, deny);
199
339
  return deny;
200
340
  }
201
- const opa = spawnSync("opa", ["eval", "-f", "json", "-I", "-d", policyPath, "data.vibepm.runapp"], { input: JSON.stringify(input), encoding: "utf-8", timeout: 1500 });
202
- if (opa.error) {
203
- const err = opa.error;
204
- const missing = err?.code === "ENOENT";
341
+ const opa = invokeSystemSync("opa", ["eval", "-f", "json", "-I", "-d", policyPath, "data.vibepm.runapp"], basePath, { input: JSON.stringify(input), timeoutMs: 1500 });
342
+ // Check for spawn errors (binary not found, etc.)
343
+ if (opa.exitCode === 127 || opa.stderr.includes("[SPAWN_ERROR]")) {
344
+ const missing = opa.stderr.includes("ENOENT") || opa.stderr.includes("not found");
205
345
  const deny = {
206
346
  allow: false,
207
347
  reasons: [
208
- missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${err.message ?? "unknown error"}`
348
+ missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${opa.stderr}`
209
349
  ],
210
350
  matched_rules: [missing ? "OPA_POLICY_MISSING" : "OPA_POLICY_EVAL_FAILED"]
211
351
  };
@@ -213,10 +353,10 @@ function evaluateOpaGate(basePath, input) {
213
353
  writeJsonBestEffort(resultPath, deny);
214
354
  return deny;
215
355
  }
216
- if (opa.status !== 0) {
356
+ if (opa.exitCode !== 0) {
217
357
  const deny = {
218
358
  allow: false,
219
- reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.status}`],
359
+ reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.exitCode}`],
220
360
  matched_rules: ["OPA_POLICY_EVAL_FAILED"]
221
361
  };
222
362
  if (resultPath)
@@ -256,11 +396,12 @@ function evaluateOpaGate(basePath, input) {
256
396
  }
257
397
  }
258
398
  function formatOpaBlockMessage(result) {
259
- const text = `${result.reasons?.join(" ") ?? ""} ${(result.matched_rules ?? []).join(" ")}`.toLowerCase();
260
- 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)) {
261
402
  return "OPA가 설치되어 있지 않거나 정책 파일이 없어 실행을 차단했습니다. OPA 설치/정책 파일을 확인하세요.";
262
403
  }
263
- if (text.includes("eval_failed")) {
404
+ if (isOpaEvalFailedError(text)) {
264
405
  return "OPA 평가에 실패하여 안전을 위해 실행을 차단했습니다. OPA 상태를 확인하세요.";
265
406
  }
266
407
  return "OPA 정책에서 허용되지 않은 커맨드/경로입니다. 정책을 만족하도록 수정하세요.";
@@ -282,19 +423,10 @@ function writeRunAppEvidenceBestEffort(repoRootAbs, payload) {
282
423
  }
283
424
  }
284
425
  async function assertPodmanAvailable() {
285
- await new Promise((resolve, reject) => {
286
- const p = spawn("podman", ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
287
- let out = "";
288
- let err = "";
289
- p.stdout.on("data", (d) => (out += String(d)));
290
- p.stderr.on("data", (d) => (err += String(d)));
291
- p.on("error", reject);
292
- p.on("close", (code) => {
293
- if (code === 0)
294
- return resolve();
295
- reject(new Error(`podman_unavailable: code=${code} out=${out} err=${err}`));
296
- });
297
- });
426
+ const result = await invokeSystem("podman", ["--version"], process.cwd());
427
+ if (result.exitCode !== 0) {
428
+ throw new Error(`podman_unavailable: code=${result.exitCode} out=${result.stdout} err=${result.stderr}`);
429
+ }
298
430
  }
299
431
  function defaultImageForProject(projectType) {
300
432
  if (projectType === "node")
@@ -328,15 +460,12 @@ function buildInsideCommand(projectInfo) {
328
460
  return run;
329
461
  }
330
462
  async function runPodmanDetached(cmd) {
331
- return await new Promise((resolve, reject) => {
332
- const p = spawn(cmd.cmd, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
333
- let out = "";
334
- let err = "";
335
- p.stdout.on("data", (d) => (out += String(d)));
336
- p.stderr.on("data", (d) => (err += String(d)));
337
- p.on("error", reject);
338
- p.on("close", (code) => resolve({ exitCode: code ?? 1, stdout: out, stderr: err }));
339
- });
463
+ const result = await invokeSystem(cmd.cmd, cmd.args, process.cwd());
464
+ return {
465
+ exitCode: result.exitCode,
466
+ stdout: result.stdout,
467
+ stderr: result.stderr
468
+ };
340
469
  }
341
470
  async function runAppWithPodman(input, repoRootAbs, mode, projectInfo) {
342
471
  const image = input.container_image ?? defaultImageForProject(projectInfo.type);
@@ -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
  }