@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.
- package/README.md +10 -10
- package/build/auth/index.js +0 -2
- package/build/auth/public_key.js +6 -4
- package/build/bootstrap/doctor.js +113 -5
- package/build/bootstrap/installer.js +85 -15
- package/build/bootstrap/registry.js +11 -6
- package/build/bootstrap/skills-installer.js +365 -0
- package/build/dx/activity.js +26 -3
- package/build/engine.js +151 -0
- package/build/errors.js +107 -0
- package/build/generated/bridge_build_seed_input.js +2 -0
- package/build/generated/bridge_build_seed_output.js +2 -0
- package/build/generated/bridge_confirm_reference_input.js +2 -0
- package/build/generated/bridge_confirm_reference_output.js +2 -0
- package/build/generated/bridge_confirmed_reference_file.js +2 -0
- package/build/generated/bridge_generate_references_input.js +2 -0
- package/build/generated/bridge_generate_references_output.js +2 -0
- package/build/generated/bridge_references_file.js +2 -0
- package/build/generated/bridge_work_order_seed_file.js +2 -0
- package/build/generated/contracts_bundle_info.js +3 -3
- package/build/generated/index.js +14 -0
- package/build/generated/ingress_input.js +2 -0
- package/build/generated/ingress_output.js +2 -0
- package/build/generated/ingress_resolution_file.js +2 -0
- package/build/generated/ingress_summary_file.js +2 -0
- package/build/generated/message_template_id_mapping_file.js +2 -0
- package/build/generated/run_app_input.js +1 -1
- package/build/index.js +4 -3
- package/build/local-mode/paths.js +1 -0
- package/build/local-mode/setup.js +21 -1
- package/build/path-utils.js +68 -0
- package/build/runtime/cli_invoker.js +1 -1
- package/build/tools/vibe_pm/advisory_review.js +5 -3
- package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
- package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
- package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
- package/build/tools/vibe_pm/briefing.js +27 -3
- package/build/tools/vibe_pm/context.js +79 -0
- package/build/tools/vibe_pm/create_work_order.js +200 -3
- package/build/tools/vibe_pm/doctor.js +95 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
- package/build/tools/vibe_pm/export_output.js +14 -13
- package/build/tools/vibe_pm/finalize_work.js +78 -40
- package/build/tools/vibe_pm/get_decision.js +2 -2
- package/build/tools/vibe_pm/index.js +128 -42
- package/build/tools/vibe_pm/ingress.js +645 -0
- package/build/tools/vibe_pm/ingress_gate.js +116 -0
- package/build/tools/vibe_pm/inspect_code.js +90 -20
- package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
- package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
- package/build/tools/vibe_pm/kce/preflight.js +11 -7
- package/build/tools/vibe_pm/memory_status.js +11 -8
- package/build/tools/vibe_pm/memory_sync.js +11 -8
- package/build/tools/vibe_pm/pm_language.js +17 -16
- package/build/tools/vibe_pm/python_error.js +115 -0
- package/build/tools/vibe_pm/run_app.js +169 -43
- package/build/tools/vibe_pm/run_app_podman.js +64 -2
- package/build/tools/vibe_pm/search_oss.js +5 -3
- package/build/tools/vibe_pm/spec_rag.js +185 -0
- package/build/tools/vibe_pm/status.js +50 -3
- package/build/tools/vibe_pm/submit_decision.js +2 -2
- package/build/tools/vibe_pm/types.js +28 -0
- package/build/tools/vibe_pm/undo_last_task.js +9 -2
- package/build/tools/vibe_pm/waiter_mapping.js +155 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
- package/build/tools.js +13 -5
- package/build/vibe-cli.js +245 -7
- package/package.json +5 -4
- package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
- 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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: "
|
|
31
|
-
message:
|
|
32
|
-
runtime
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
68
|
+
const res = await runPythonCli(cmd, { timeoutMs: 180_000 });
|
|
68
69
|
if (res.code !== 0) {
|
|
69
|
-
|
|
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) {
|