@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.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- package/package.json +76 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/run_app.ts
|
|
2
|
+
// vibe_pm.run_app - One-click application launch
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { resolveRunDir, resolveRunId } from "./context.js";
|
|
8
|
+
import { buildPodmanRunCommand, filterContainerEnvAllowlist, normalizeMounts, normalizePorts } from "./run_app_podman.js";
|
|
9
|
+
/**
|
|
10
|
+
* vibe_pm.run_app - Auto-detect project type and run
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Detect project type (Node.js vs Python)
|
|
14
|
+
* 2. Determine run command based on mode
|
|
15
|
+
* 3. Start process in background
|
|
16
|
+
* 4. Return URL and PID
|
|
17
|
+
*/
|
|
18
|
+
export async function runApp(input) {
|
|
19
|
+
const basePath = process.cwd();
|
|
20
|
+
const mode = input.mode ?? "dev";
|
|
21
|
+
const customPort = input.port;
|
|
22
|
+
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") {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
command_executed: "detect_project",
|
|
29
|
+
message: "프로젝트 유형을 감지할 수 없습니다. package.json 또는 requirements.txt가 필요합니다.",
|
|
30
|
+
runtime: "host"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (container === "podman") {
|
|
34
|
+
return await runAppWithPodman(input, basePath, mode, projectInfo);
|
|
35
|
+
}
|
|
36
|
+
const opaInput = buildOpaPolicyInput(basePath, projectInfo.command, projectInfo.args, {
|
|
37
|
+
paths: [basePath],
|
|
38
|
+
runtime: "host"
|
|
39
|
+
});
|
|
40
|
+
const opaDecision = evaluateOpaGate(basePath, opaInput);
|
|
41
|
+
if (!opaDecision.allow) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
command_executed: `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim(),
|
|
45
|
+
message: formatOpaBlockMessage(opaDecision),
|
|
46
|
+
runtime: "host"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Step 2: Start process
|
|
50
|
+
const fullCommand = `${projectInfo.command} ${projectInfo.args.join(" ")}`.trim();
|
|
51
|
+
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
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// Unref to allow parent to exit independently
|
|
63
|
+
child.unref();
|
|
64
|
+
const pid = child.pid;
|
|
65
|
+
// Give the process a moment to start
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
67
|
+
writeRunAppEvidenceBestEffort(basePath, {
|
|
68
|
+
runtime: "host",
|
|
69
|
+
mode,
|
|
70
|
+
port: projectInfo.port,
|
|
71
|
+
url: projectInfo.url,
|
|
72
|
+
command_executed: fullCommand,
|
|
73
|
+
process_id: pid
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
process_id: pid,
|
|
78
|
+
url: projectInfo.url,
|
|
79
|
+
command_executed: fullCommand,
|
|
80
|
+
message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
|
|
81
|
+
runtime: "host"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
command_executed: fullCommand,
|
|
88
|
+
message: `실행 실패: ${err instanceof Error ? err.message : String(err)}`,
|
|
89
|
+
runtime: "host"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function stableShortHash(input) {
|
|
94
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 12);
|
|
95
|
+
}
|
|
96
|
+
function readCurrentWorkOrderRunId(basePath) {
|
|
97
|
+
const workOrderPath = path.join(basePath, ".vibe", "state", "current_work_order.json");
|
|
98
|
+
if (!fs.existsSync(workOrderPath))
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
const raw = JSON.parse(fs.readFileSync(workOrderPath, "utf-8"));
|
|
102
|
+
const runId = typeof raw?.run_id === "string" ? raw.run_id.trim() : "";
|
|
103
|
+
if (!runId)
|
|
104
|
+
return null;
|
|
105
|
+
if (runId.includes("..") || runId.includes("/") || runId.includes("\\") || runId.includes("\0")) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return runId;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function resolveRunIdForSecurity(basePath) {
|
|
115
|
+
return readCurrentWorkOrderRunId(basePath) ?? resolveRunId(undefined, basePath).run_id;
|
|
116
|
+
}
|
|
117
|
+
function resolveSecurityDir(basePath, runId) {
|
|
118
|
+
try {
|
|
119
|
+
const resolved = resolveRunDir(runId, basePath);
|
|
120
|
+
const runDir = resolved?.run_dir ?? path.join(basePath, "runs", runId);
|
|
121
|
+
const securityDir = path.join(runDir, "security");
|
|
122
|
+
fs.mkdirSync(securityDir, { recursive: true });
|
|
123
|
+
return securityDir;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function writeJsonBestEffort(filePath, payload) {
|
|
130
|
+
try {
|
|
131
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
132
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
}
|
|
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;
|
|
162
|
+
}
|
|
163
|
+
function buildOpaPolicyInput(basePath, command, args, opts) {
|
|
164
|
+
const cwd = normalizeOpaPath(basePath);
|
|
165
|
+
const paths = normalizeOpaPaths(basePath, opts?.paths ?? [basePath]);
|
|
166
|
+
const mounts = opts?.mounts ? normalizeOpaPaths(basePath, opts.mounts) : undefined;
|
|
167
|
+
return {
|
|
168
|
+
action: "run_app",
|
|
169
|
+
project_root: cwd,
|
|
170
|
+
request: {
|
|
171
|
+
command,
|
|
172
|
+
args,
|
|
173
|
+
cwd,
|
|
174
|
+
paths,
|
|
175
|
+
mounts
|
|
176
|
+
},
|
|
177
|
+
meta: {
|
|
178
|
+
tool: "vibe_pm.run_app",
|
|
179
|
+
runtime: opts?.runtime ?? "host"
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function evaluateOpaGate(basePath, input) {
|
|
184
|
+
const runId = resolveRunIdForSecurity(basePath);
|
|
185
|
+
const securityDir = resolveSecurityDir(basePath, runId);
|
|
186
|
+
const inputPath = securityDir ? path.join(securityDir, "policy_input.json") : null;
|
|
187
|
+
const resultPath = securityDir ? path.join(securityDir, "policy_result.json") : null;
|
|
188
|
+
if (inputPath)
|
|
189
|
+
writeJsonBestEffort(inputPath, input);
|
|
190
|
+
const policyPath = path.join(basePath, "policy", "vibepm", "run_app.rego");
|
|
191
|
+
if (!fs.existsSync(policyPath)) {
|
|
192
|
+
const deny = {
|
|
193
|
+
allow: false,
|
|
194
|
+
reasons: ["OPA_POLICY_MISSING: policy file missing"],
|
|
195
|
+
matched_rules: ["OPA_POLICY_MISSING"]
|
|
196
|
+
};
|
|
197
|
+
if (resultPath)
|
|
198
|
+
writeJsonBestEffort(resultPath, deny);
|
|
199
|
+
return deny;
|
|
200
|
+
}
|
|
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";
|
|
205
|
+
const deny = {
|
|
206
|
+
allow: false,
|
|
207
|
+
reasons: [
|
|
208
|
+
missing ? "OPA_POLICY_MISSING: opa binary not found" : `OPA_POLICY_EVAL_FAILED: ${err.message ?? "unknown error"}`
|
|
209
|
+
],
|
|
210
|
+
matched_rules: [missing ? "OPA_POLICY_MISSING" : "OPA_POLICY_EVAL_FAILED"]
|
|
211
|
+
};
|
|
212
|
+
if (resultPath)
|
|
213
|
+
writeJsonBestEffort(resultPath, deny);
|
|
214
|
+
return deny;
|
|
215
|
+
}
|
|
216
|
+
if (opa.status !== 0) {
|
|
217
|
+
const deny = {
|
|
218
|
+
allow: false,
|
|
219
|
+
reasons: [`OPA_POLICY_EVAL_FAILED: exit_code=${opa.status}`],
|
|
220
|
+
matched_rules: ["OPA_POLICY_EVAL_FAILED"]
|
|
221
|
+
};
|
|
222
|
+
if (resultPath)
|
|
223
|
+
writeJsonBestEffort(resultPath, deny);
|
|
224
|
+
return deny;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(opa.stdout || "{}");
|
|
228
|
+
const value = parsed?.result?.[0]?.expressions?.[0]?.value ?? {};
|
|
229
|
+
const allow = Boolean(value?.allow);
|
|
230
|
+
const reasons = Array.isArray(value?.reasons) ? value.reasons : [];
|
|
231
|
+
const matched_rules = Array.isArray(value?.matched_rules) ? value.matched_rules : [];
|
|
232
|
+
const result = { allow, reasons, matched_rules };
|
|
233
|
+
if (typeof value?.allow !== "boolean") {
|
|
234
|
+
const deny = {
|
|
235
|
+
allow: false,
|
|
236
|
+
reasons: ["OPA_POLICY_EVAL_FAILED: missing allow result"],
|
|
237
|
+
matched_rules: ["OPA_POLICY_EVAL_FAILED"]
|
|
238
|
+
};
|
|
239
|
+
if (resultPath)
|
|
240
|
+
writeJsonBestEffort(resultPath, deny);
|
|
241
|
+
return deny;
|
|
242
|
+
}
|
|
243
|
+
if (resultPath)
|
|
244
|
+
writeJsonBestEffort(resultPath, result);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
const deny = {
|
|
249
|
+
allow: false,
|
|
250
|
+
reasons: ["OPA_POLICY_EVAL_FAILED: invalid JSON output"],
|
|
251
|
+
matched_rules: ["OPA_POLICY_EVAL_FAILED"]
|
|
252
|
+
};
|
|
253
|
+
if (resultPath)
|
|
254
|
+
writeJsonBestEffort(resultPath, deny);
|
|
255
|
+
return deny;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function formatOpaBlockMessage(result) {
|
|
259
|
+
const text = `${result.reasons?.join(" ") ?? ""} ${(result.matched_rules ?? []).join(" ")}`.toLowerCase();
|
|
260
|
+
if (text.includes("missing")) {
|
|
261
|
+
return "OPA가 설치되어 있지 않거나 정책 파일이 없어 실행을 차단했습니다. OPA 설치/정책 파일을 확인하세요.";
|
|
262
|
+
}
|
|
263
|
+
if (text.includes("eval_failed")) {
|
|
264
|
+
return "OPA 평가에 실패하여 안전을 위해 실행을 차단했습니다. OPA 상태를 확인하세요.";
|
|
265
|
+
}
|
|
266
|
+
return "OPA 정책에서 허용되지 않은 커맨드/경로입니다. 정책을 만족하도록 수정하세요.";
|
|
267
|
+
}
|
|
268
|
+
function ensureDir(dirPath) {
|
|
269
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
270
|
+
}
|
|
271
|
+
function writeRunAppEvidenceBestEffort(repoRootAbs, payload) {
|
|
272
|
+
try {
|
|
273
|
+
const dir = path.join(repoRootAbs, ".vibe", "evidence");
|
|
274
|
+
ensureDir(dir);
|
|
275
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
276
|
+
const file = path.join(dir, `run_app_runtime_${ts}.json`);
|
|
277
|
+
fs.writeFileSync(file, JSON.stringify(payload, null, 2), "utf-8");
|
|
278
|
+
return file;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
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
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function defaultImageForProject(projectType) {
|
|
300
|
+
if (projectType === "node")
|
|
301
|
+
return "docker.io/library/node:20-bookworm";
|
|
302
|
+
if (projectType === "python")
|
|
303
|
+
return "docker.io/library/python:3.11-slim";
|
|
304
|
+
return "docker.io/library/ubuntu:24.04";
|
|
305
|
+
}
|
|
306
|
+
function shellQuoteArg(arg) {
|
|
307
|
+
// Best-effort: single-quote wrapping for /bin/sh -lc
|
|
308
|
+
return `'${arg.replace(/'/g, `'\"'\"'`)}'`;
|
|
309
|
+
}
|
|
310
|
+
function buildInsideCommand(projectInfo) {
|
|
311
|
+
const cmd = projectInfo.command;
|
|
312
|
+
const args = projectInfo.args.map(shellQuoteArg).join(" ");
|
|
313
|
+
const run = `${cmd} ${args}`.trim();
|
|
314
|
+
if (projectInfo.type === "node") {
|
|
315
|
+
// Avoid polluting host node_modules by mounting a volume at /work/node_modules.
|
|
316
|
+
// Install only if node_modules is missing.
|
|
317
|
+
return `if [ ! -d node_modules ]; then if [ -f package-lock.json ]; then npm ci; else npm install; fi; fi; ${run}`;
|
|
318
|
+
}
|
|
319
|
+
if (projectInfo.type === "python") {
|
|
320
|
+
// Install dependencies best-effort every time (pip cache volume reduces cost).
|
|
321
|
+
// If command is "uvicorn", prefer python -m uvicorn so requirements installation covers it.
|
|
322
|
+
if (cmd === "uvicorn") {
|
|
323
|
+
const uvArgs = projectInfo.args.map(shellQuoteArg).join(" ");
|
|
324
|
+
return `python -m pip install -r requirements.txt && python -m uvicorn ${uvArgs}`.trim();
|
|
325
|
+
}
|
|
326
|
+
return `python -m pip install -r requirements.txt && ${run}`;
|
|
327
|
+
}
|
|
328
|
+
return run;
|
|
329
|
+
}
|
|
330
|
+
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
|
+
});
|
|
340
|
+
}
|
|
341
|
+
async function runAppWithPodman(input, repoRootAbs, mode, projectInfo) {
|
|
342
|
+
const image = input.container_image ?? defaultImageForProject(projectInfo.type);
|
|
343
|
+
const runId = stableShortHash(`${repoRootAbs}:${Date.now()}`);
|
|
344
|
+
// mounts & ports
|
|
345
|
+
let mounts;
|
|
346
|
+
try {
|
|
347
|
+
mounts = normalizeMounts(repoRootAbs, input.container_mounts);
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
command_executed: "podman run",
|
|
353
|
+
message: `컨테이너 마운트 설정이 허용되지 않습니다: ${e instanceof Error ? e.message : String(e)}`,
|
|
354
|
+
runtime: "podman"
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const ports = normalizePorts(projectInfo.port, input.container_ports);
|
|
358
|
+
// env (allowlist + defaults)
|
|
359
|
+
const userEnv = filterContainerEnvAllowlist(input.container_env);
|
|
360
|
+
const env = {
|
|
361
|
+
...userEnv,
|
|
362
|
+
PORT: String(projectInfo.port)
|
|
363
|
+
};
|
|
364
|
+
if (projectInfo.type === "node") {
|
|
365
|
+
env.NODE_ENV = mode === "prod" ? "production" : "development";
|
|
366
|
+
}
|
|
367
|
+
if (projectInfo.type === "python") {
|
|
368
|
+
env.PYTHONUNBUFFERED = env.PYTHONUNBUFFERED ?? "1";
|
|
369
|
+
}
|
|
370
|
+
// extra volumes (cache + node_modules safety)
|
|
371
|
+
const repoHash = stableShortHash(repoRootAbs);
|
|
372
|
+
const extraVolumes = [];
|
|
373
|
+
if (projectInfo.type === "node") {
|
|
374
|
+
extraVolumes.push({ name: `vibe-${repoHash}-node-modules`, containerPath: "/work/node_modules" }, { name: `vibe-${repoHash}-npm-cache`, containerPath: "/root/.npm" });
|
|
375
|
+
}
|
|
376
|
+
if (projectInfo.type === "python") {
|
|
377
|
+
extraVolumes.push({ name: `vibe-${repoHash}-pip-cache`, containerPath: "/root/.cache/pip" });
|
|
378
|
+
}
|
|
379
|
+
const insideCommand = buildInsideCommand(projectInfo);
|
|
380
|
+
const pod = buildPodmanRunCommand({
|
|
381
|
+
runNameSuffix: runId,
|
|
382
|
+
image,
|
|
383
|
+
mounts,
|
|
384
|
+
ports,
|
|
385
|
+
env,
|
|
386
|
+
insideCommand,
|
|
387
|
+
extraVolumes
|
|
388
|
+
});
|
|
389
|
+
const opaInput = buildOpaPolicyInput(repoRootAbs, pod.cmd, pod.args, {
|
|
390
|
+
paths: [repoRootAbs, ...mounts.map((m) => m.hostAbs)],
|
|
391
|
+
mounts: mounts.map((m) => m.hostAbs),
|
|
392
|
+
runtime: "podman"
|
|
393
|
+
});
|
|
394
|
+
const opaDecision = evaluateOpaGate(repoRootAbs, opaInput);
|
|
395
|
+
if (!opaDecision.allow) {
|
|
396
|
+
return {
|
|
397
|
+
success: false,
|
|
398
|
+
command_executed: pod.command_executed,
|
|
399
|
+
message: formatOpaBlockMessage(opaDecision),
|
|
400
|
+
runtime: "podman"
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
await assertPodmanAvailable();
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
return {
|
|
408
|
+
success: false,
|
|
409
|
+
command_executed: "podman --version",
|
|
410
|
+
message: `Podman 실행 불가: ${e instanceof Error ? e.message : String(e)}. container 옵션을 끄고(host) 다시 실행하세요.`,
|
|
411
|
+
runtime: "podman"
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const evidencePath = writeRunAppEvidenceBestEffort(repoRootAbs, {
|
|
415
|
+
runtime: "podman",
|
|
416
|
+
mode,
|
|
417
|
+
image,
|
|
418
|
+
ports,
|
|
419
|
+
env_keys: Object.keys(env),
|
|
420
|
+
mounts,
|
|
421
|
+
extra_volumes: extraVolumes,
|
|
422
|
+
command_executed: pod.command_executed
|
|
423
|
+
});
|
|
424
|
+
const started = await runPodmanDetached(pod);
|
|
425
|
+
if (started.exitCode !== 0) {
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
command_executed: pod.command_executed,
|
|
429
|
+
message: `Podman 실행 실패(exit=${started.exitCode}). ${started.stderr.trim()}`,
|
|
430
|
+
runtime: "podman"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const containerId = started.stdout.trim().split(/\s+/)[0] || undefined;
|
|
434
|
+
// Give the container a moment to start
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
436
|
+
writeRunAppEvidenceBestEffort(repoRootAbs, {
|
|
437
|
+
runtime: "podman",
|
|
438
|
+
container_id: containerId,
|
|
439
|
+
url: projectInfo.url,
|
|
440
|
+
evidence: evidencePath
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
success: true,
|
|
444
|
+
url: projectInfo.url,
|
|
445
|
+
command_executed: pod.command_executed,
|
|
446
|
+
message: `애플리케이션이 시작되었습니다. ${projectInfo.url} 에서 확인하세요.`,
|
|
447
|
+
runtime: "podman",
|
|
448
|
+
container_id: containerId
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Detect project type and determine run command
|
|
453
|
+
*/
|
|
454
|
+
function detectProject(basePath, mode, customPort) {
|
|
455
|
+
const packageJsonPath = path.join(basePath, "package.json");
|
|
456
|
+
const requirementsPath = path.join(basePath, "requirements.txt");
|
|
457
|
+
// Check for Node.js project
|
|
458
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
459
|
+
return detectNodeProject(basePath, packageJsonPath, mode, customPort);
|
|
460
|
+
}
|
|
461
|
+
// Check for Python project
|
|
462
|
+
if (fs.existsSync(requirementsPath)) {
|
|
463
|
+
return detectPythonProject(basePath, mode, customPort);
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
type: "unknown",
|
|
467
|
+
command: "",
|
|
468
|
+
args: [],
|
|
469
|
+
port: 3000,
|
|
470
|
+
url: ""
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Detect Node.js project run command
|
|
475
|
+
*/
|
|
476
|
+
function detectNodeProject(basePath, packageJsonPath, mode, customPort) {
|
|
477
|
+
const port = customPort ?? 3000;
|
|
478
|
+
const url = `http://localhost:${port}`;
|
|
479
|
+
try {
|
|
480
|
+
const content = fs.readFileSync(packageJsonPath, "utf-8");
|
|
481
|
+
const pkg = JSON.parse(content);
|
|
482
|
+
const scripts = pkg.scripts ?? {};
|
|
483
|
+
// Determine command based on mode and available scripts
|
|
484
|
+
if (mode === "test" && scripts.test) {
|
|
485
|
+
return { type: "node", command: "npm", args: ["run", "test"], port, url };
|
|
486
|
+
}
|
|
487
|
+
if (mode === "prod") {
|
|
488
|
+
if (scripts.start) {
|
|
489
|
+
return { type: "node", command: "npm", args: ["start"], port, url };
|
|
490
|
+
}
|
|
491
|
+
if (scripts.build && scripts.preview) {
|
|
492
|
+
return { type: "node", command: "npm", args: ["run", "preview"], port, url };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Dev mode (default)
|
|
496
|
+
if (scripts.dev) {
|
|
497
|
+
return { type: "node", command: "npm", args: ["run", "dev"], port, url };
|
|
498
|
+
}
|
|
499
|
+
if (scripts.start) {
|
|
500
|
+
return { type: "node", command: "npm", args: ["start"], port, url };
|
|
501
|
+
}
|
|
502
|
+
// Fallback: run main file directly
|
|
503
|
+
const mainFile = pkg.main ?? "index.js";
|
|
504
|
+
if (fs.existsSync(path.join(basePath, mainFile))) {
|
|
505
|
+
return { type: "node", command: "node", args: [mainFile], port, url };
|
|
506
|
+
}
|
|
507
|
+
// Try common entry points
|
|
508
|
+
const commonEntries = ["src/index.js", "src/index.ts", "index.ts", "main.js", "server.js", "app.js"];
|
|
509
|
+
for (const entry of commonEntries) {
|
|
510
|
+
if (fs.existsSync(path.join(basePath, entry))) {
|
|
511
|
+
const runner = entry.endsWith(".ts") ? "npx" : "node";
|
|
512
|
+
const runArgs = entry.endsWith(".ts") ? ["tsx", entry] : [entry];
|
|
513
|
+
return { type: "node", command: runner, args: runArgs, port, url };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
// Fall through to default
|
|
519
|
+
}
|
|
520
|
+
// Default Node.js command
|
|
521
|
+
return { type: "node", command: "npm", args: ["start"], port, url };
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Detect Python project run command
|
|
525
|
+
*/
|
|
526
|
+
function detectPythonProject(basePath, mode, customPort) {
|
|
527
|
+
const port = customPort ?? 8000;
|
|
528
|
+
const url = `http://localhost:${port}`;
|
|
529
|
+
// Check for test mode
|
|
530
|
+
if (mode === "test") {
|
|
531
|
+
if (fs.existsSync(path.join(basePath, "pytest.ini")) ||
|
|
532
|
+
fs.existsSync(path.join(basePath, "tests"))) {
|
|
533
|
+
return { type: "python", command: "python", args: ["-m", "pytest"], port, url };
|
|
534
|
+
}
|
|
535
|
+
return { type: "python", command: "python", args: ["-m", "unittest"], port, url };
|
|
536
|
+
}
|
|
537
|
+
// Check for Django
|
|
538
|
+
const managePyPath = path.join(basePath, "manage.py");
|
|
539
|
+
if (fs.existsSync(managePyPath)) {
|
|
540
|
+
return {
|
|
541
|
+
type: "python",
|
|
542
|
+
command: "python",
|
|
543
|
+
args: ["manage.py", "runserver", `0.0.0.0:${port}`],
|
|
544
|
+
port,
|
|
545
|
+
url
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// Check for Flask
|
|
549
|
+
const appPyPath = path.join(basePath, "app.py");
|
|
550
|
+
if (fs.existsSync(appPyPath)) {
|
|
551
|
+
// Check if it's a Flask app
|
|
552
|
+
try {
|
|
553
|
+
const content = fs.readFileSync(appPyPath, "utf-8");
|
|
554
|
+
if (content.includes("flask") || content.includes("Flask")) {
|
|
555
|
+
return {
|
|
556
|
+
type: "python",
|
|
557
|
+
command: "python",
|
|
558
|
+
args: ["-m", "flask", "run", "--host=0.0.0.0", `--port=${port}`],
|
|
559
|
+
port,
|
|
560
|
+
url
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// Fall through
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Check for FastAPI
|
|
569
|
+
const mainPyPath = path.join(basePath, "main.py");
|
|
570
|
+
if (fs.existsSync(mainPyPath)) {
|
|
571
|
+
try {
|
|
572
|
+
const content = fs.readFileSync(mainPyPath, "utf-8");
|
|
573
|
+
if (content.includes("fastapi") || content.includes("FastAPI")) {
|
|
574
|
+
return {
|
|
575
|
+
type: "python",
|
|
576
|
+
command: "uvicorn",
|
|
577
|
+
args: ["main:app", "--host", "0.0.0.0", "--port", String(port), "--reload"],
|
|
578
|
+
port,
|
|
579
|
+
url
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Fall through
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// Default: run main.py
|
|
588
|
+
if (fs.existsSync(mainPyPath)) {
|
|
589
|
+
return { type: "python", command: "python", args: ["main.py"], port, url };
|
|
590
|
+
}
|
|
591
|
+
// Try app.py
|
|
592
|
+
if (fs.existsSync(appPyPath)) {
|
|
593
|
+
return { type: "python", command: "python", args: ["app.py"], port, url };
|
|
594
|
+
}
|
|
595
|
+
// Fallback
|
|
596
|
+
return { type: "python", command: "python", args: ["main.py"], port, url };
|
|
597
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// adapters/mcp-ts/src/tools/vibe_pm/run_app_podman.ts
|
|
2
|
+
// Pure helpers for Podman run_app path (testable without spawning Podman)
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
export const CONTAINER_ENV_ALLOWLIST = new Set(["NODE_ENV", "PORT", "PYTHONUNBUFFERED", "CI"]);
|
|
5
|
+
export function filterContainerEnvAllowlist(env) {
|
|
6
|
+
const out = {};
|
|
7
|
+
if (!env)
|
|
8
|
+
return out;
|
|
9
|
+
for (const [k, v] of Object.entries(env)) {
|
|
10
|
+
if (CONTAINER_ENV_ALLOWLIST.has(k))
|
|
11
|
+
out[k] = v;
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function isSubPath(childAbs, parentAbs) {
|
|
16
|
+
const rel = path.relative(parentAbs, childAbs);
|
|
17
|
+
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
18
|
+
}
|
|
19
|
+
export function normalizeMounts(repoRootAbs, mounts) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const m of mounts ?? []) {
|
|
22
|
+
const hostAbs = path.isAbsolute(m.host) ? m.host : path.resolve(repoRootAbs, m.host);
|
|
23
|
+
if (hostAbs !== repoRootAbs && !isSubPath(hostAbs, repoRootAbs)) {
|
|
24
|
+
throw new Error(`mount_escape_blocked: host=${hostAbs}`);
|
|
25
|
+
}
|
|
26
|
+
out.push({ hostAbs, container: m.container, mode: m.mode });
|
|
27
|
+
}
|
|
28
|
+
if (!out.some((m) => m.container === "/work")) {
|
|
29
|
+
out.unshift({ hostAbs: repoRootAbs, container: "/work", mode: "rw" });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function normalizePorts(primary, ports) {
|
|
34
|
+
const out = new Set([primary]);
|
|
35
|
+
for (const p of ports ?? [])
|
|
36
|
+
out.add(p);
|
|
37
|
+
return [...out];
|
|
38
|
+
}
|
|
39
|
+
export function buildPodmanRunCommand(args) {
|
|
40
|
+
const podArgs = [
|
|
41
|
+
"run",
|
|
42
|
+
"-d",
|
|
43
|
+
"--rm",
|
|
44
|
+
"--name",
|
|
45
|
+
`vibe-run-${args.runNameSuffix}`,
|
|
46
|
+
"-w",
|
|
47
|
+
"/work"
|
|
48
|
+
];
|
|
49
|
+
for (const m of args.mounts) {
|
|
50
|
+
podArgs.push("-v", `${m.hostAbs}:${m.container}:${m.mode}`);
|
|
51
|
+
}
|
|
52
|
+
for (const v of args.extraVolumes) {
|
|
53
|
+
podArgs.push("-v", `${v.name}:${v.containerPath}:rw`);
|
|
54
|
+
}
|
|
55
|
+
for (const [k, v] of Object.entries(args.env)) {
|
|
56
|
+
podArgs.push("-e", `${k}=${v}`);
|
|
57
|
+
}
|
|
58
|
+
for (const p of args.ports) {
|
|
59
|
+
podArgs.push("-p", `${p}:${p}`);
|
|
60
|
+
}
|
|
61
|
+
podArgs.push("--pull=missing");
|
|
62
|
+
podArgs.push(args.image, "sh", "-lc", args.insideCommand);
|
|
63
|
+
return { cmd: "podman", args: podArgs, command_executed: ["podman", ...podArgs].join(" ") };
|
|
64
|
+
}
|