karajan-code 1.34.4 → 1.35.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/package.json +1 -1
- package/src/bootstrap.js +235 -0
- package/src/config.js +1 -1
- package/src/mcp/server-handlers.js +33 -2
- package/src/orchestrator/preflight-checks.js +54 -27
- package/src/orchestrator.js +9 -2
- package/src/sonar/credentials.js +35 -0
- package/src/sonar/scanner.js +8 -7
package/package.json
CHANGED
package/src/bootstrap.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project bootstrap — mandatory prerequisite gate.
|
|
3
|
+
*
|
|
4
|
+
* Before any KJ tool that executes agents (kj_run, kj_code, kj_review, etc.),
|
|
5
|
+
* this module validates that ALL environment prerequisites are met.
|
|
6
|
+
*
|
|
7
|
+
* Philosophy: NEVER degrade gracefully. If something is missing, STOP and
|
|
8
|
+
* tell the user exactly what to fix. No silent fallbacks, no auto-disabling.
|
|
9
|
+
*
|
|
10
|
+
* Results are cached in `.kj-ready.json` per project (TTL-based) so that
|
|
11
|
+
* slow checks (SonarQube reachability, agent detection) don't repeat on
|
|
12
|
+
* every invocation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { ensureGitRepo } from "./utils/git.js";
|
|
20
|
+
import { runCommand } from "./utils/process.js";
|
|
21
|
+
import { checkBinary } from "./utils/agent-detect.js";
|
|
22
|
+
import { exists } from "./utils/fs.js";
|
|
23
|
+
import { getConfigPath, resolveRole } from "./config.js";
|
|
24
|
+
import { isSonarReachable, sonarUp } from "./sonar/manager.js";
|
|
25
|
+
|
|
26
|
+
const BOOTSTRAP_VERSION = 1;
|
|
27
|
+
const BOOTSTRAP_TTL_HOURS = 24;
|
|
28
|
+
const BOOTSTRAP_FILENAME = ".kj-ready.json";
|
|
29
|
+
|
|
30
|
+
function getPackageVersion() {
|
|
31
|
+
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
32
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Individual checks ────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
async function checkGitRepo() {
|
|
38
|
+
let ok = false;
|
|
39
|
+
try {
|
|
40
|
+
ok = await ensureGitRepo();
|
|
41
|
+
} catch {
|
|
42
|
+
ok = false;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
name: "gitRepo",
|
|
46
|
+
ok,
|
|
47
|
+
detail: ok ? "Inside a git repository" : "Not a git repository",
|
|
48
|
+
fix: "Run 'git init' in your project directory."
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function checkGitRemote() {
|
|
53
|
+
try {
|
|
54
|
+
const res = await runCommand("git", ["remote", "get-url", "origin"]);
|
|
55
|
+
if (res.exitCode === 0 && res.stdout.trim()) {
|
|
56
|
+
return { name: "gitRemote", ok: true, detail: res.stdout.trim(), fix: null };
|
|
57
|
+
}
|
|
58
|
+
} catch { /* fall through */ }
|
|
59
|
+
return {
|
|
60
|
+
name: "gitRemote",
|
|
61
|
+
ok: false,
|
|
62
|
+
detail: "origin remote not configured",
|
|
63
|
+
fix: "Run 'git remote add origin <your-repo-url>'."
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function checkConfigExists() {
|
|
68
|
+
const configPath = getConfigPath();
|
|
69
|
+
const configOk = await exists(configPath);
|
|
70
|
+
return {
|
|
71
|
+
name: "config",
|
|
72
|
+
ok: configOk,
|
|
73
|
+
detail: configOk ? configPath : "Config file not found",
|
|
74
|
+
fix: configOk ? null : "Run 'kj_init' to create your Karajan config file."
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function checkCoreBinaries() {
|
|
79
|
+
const missing = [];
|
|
80
|
+
for (const bin of ["node", "npm", "git"]) {
|
|
81
|
+
const result = await checkBinary(bin);
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
missing.push(bin);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (missing.length > 0) {
|
|
87
|
+
return {
|
|
88
|
+
name: "coreBinaries",
|
|
89
|
+
ok: false,
|
|
90
|
+
detail: `Missing: ${missing.join(", ")}`,
|
|
91
|
+
fix: `Install missing binaries: ${missing.join(", ")}.`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { name: "coreBinaries", ok: true, detail: "node, npm, git available", fix: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function checkConfiguredAgents(config) {
|
|
98
|
+
const { provider } = resolveRole(config, "coder");
|
|
99
|
+
if (!provider) {
|
|
100
|
+
return {
|
|
101
|
+
name: "agents",
|
|
102
|
+
ok: false,
|
|
103
|
+
detail: "No coder provider configured",
|
|
104
|
+
fix: "Run 'kj_init' or set a coder provider in kj.config.yml."
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const result = await checkBinary(provider);
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
return {
|
|
110
|
+
name: "agents",
|
|
111
|
+
ok: false,
|
|
112
|
+
detail: `Coder agent "${provider}" not found`,
|
|
113
|
+
fix: `Install "${provider}" CLI. Run 'kj_doctor' for installation instructions.`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { name: "agents", ok: true, detail: `coder: ${provider}`, fix: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkSonarQubeReady(config) {
|
|
120
|
+
if (config.sonarqube?.enabled === false) {
|
|
121
|
+
return { name: "sonarqube", ok: true, detail: "Disabled in config", fix: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const host = config.sonarqube?.host || "http://localhost:9000";
|
|
125
|
+
|
|
126
|
+
// First attempt
|
|
127
|
+
let reachable = await isSonarReachable(host);
|
|
128
|
+
if (reachable) {
|
|
129
|
+
return { name: "sonarqube", ok: true, detail: `Reachable at ${host}`, fix: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Auto-remediation: try to start
|
|
133
|
+
try {
|
|
134
|
+
await sonarUp(host);
|
|
135
|
+
reachable = await isSonarReachable(host);
|
|
136
|
+
if (reachable) {
|
|
137
|
+
return { name: "sonarqube", ok: true, detail: `Started and reachable at ${host}`, fix: null };
|
|
138
|
+
}
|
|
139
|
+
} catch { /* fall through */ }
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name: "sonarqube",
|
|
143
|
+
ok: false,
|
|
144
|
+
detail: `Not reachable at ${host}`,
|
|
145
|
+
fix: `Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar.`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Bootstrap file management ────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function bootstrapPath(projectDir) {
|
|
152
|
+
return path.join(projectDir, BOOTSTRAP_FILENAME);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readBootstrapFile(projectDir) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = await fs.readFile(bootstrapPath(projectDir), "utf8");
|
|
158
|
+
return JSON.parse(raw);
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isBootstrapValid(bootstrap, projectDir) {
|
|
165
|
+
if (!bootstrap || bootstrap.version !== BOOTSTRAP_VERSION) return false;
|
|
166
|
+
if (bootstrap.karajanVersion !== getPackageVersion()) return false;
|
|
167
|
+
if (bootstrap.projectDir !== projectDir) return false;
|
|
168
|
+
const age = Date.now() - new Date(bootstrap.createdAt).getTime();
|
|
169
|
+
if (age > BOOTSTRAP_TTL_HOURS * 3600 * 1000) return false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function writeBootstrapFile(projectDir, checks) {
|
|
174
|
+
const data = {
|
|
175
|
+
version: BOOTSTRAP_VERSION,
|
|
176
|
+
karajanVersion: getPackageVersion(),
|
|
177
|
+
createdAt: new Date().toISOString(),
|
|
178
|
+
projectDir,
|
|
179
|
+
checks: Object.fromEntries(checks.map(c => [c.name, { ok: c.ok, detail: c.detail }]))
|
|
180
|
+
};
|
|
181
|
+
await fs.writeFile(bootstrapPath(projectDir), JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatBootstrapFailure(failures) {
|
|
185
|
+
const lines = failures.map(f =>
|
|
186
|
+
` FAIL ${f.name}: ${f.detail}\n Fix: ${f.fix}`
|
|
187
|
+
);
|
|
188
|
+
return [
|
|
189
|
+
"BOOTSTRAP FAILED — Environment not ready for Karajan Code.\n",
|
|
190
|
+
"The following prerequisite(s) are not met:\n",
|
|
191
|
+
...lines,
|
|
192
|
+
"",
|
|
193
|
+
"Run 'kj_doctor' for a complete environment diagnostic.",
|
|
194
|
+
"Do NOT work around these issues — fix them properly."
|
|
195
|
+
].join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Ensure the project environment is ready for KJ execution.
|
|
202
|
+
* Reads cached `.kj-ready.json` if valid; otherwise runs all checks.
|
|
203
|
+
* Throws Error with actionable message if any prerequisite fails.
|
|
204
|
+
*/
|
|
205
|
+
export async function ensureBootstrap(projectDir, config) {
|
|
206
|
+
const cached = await readBootstrapFile(projectDir);
|
|
207
|
+
if (isBootstrapValid(cached, projectDir)) {
|
|
208
|
+
return; // Environment already validated
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const checks = await Promise.all([
|
|
212
|
+
checkGitRepo(),
|
|
213
|
+
checkGitRemote(),
|
|
214
|
+
checkConfigExists(),
|
|
215
|
+
checkCoreBinaries(),
|
|
216
|
+
checkConfiguredAgents(config),
|
|
217
|
+
checkSonarQubeReady(config)
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const failures = checks.filter(c => !c.ok);
|
|
221
|
+
if (failures.length > 0) {
|
|
222
|
+
throw new Error(formatBootstrapFailure(failures));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await writeBootstrapFile(projectDir, checks);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete `.kj-ready.json` to force re-validation on next run.
|
|
230
|
+
*/
|
|
231
|
+
export async function invalidateBootstrap(projectDir) {
|
|
232
|
+
try {
|
|
233
|
+
await fs.unlink(bootstrapPath(projectDir));
|
|
234
|
+
} catch { /* file may not exist */ }
|
|
235
|
+
}
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,7 @@ import { resolveReviewProfile } from "../review/profiles.js";
|
|
|
24
24
|
import { createRunLog, readRunLog } from "../utils/run-log.js";
|
|
25
25
|
import { currentBranch } from "../utils/git.js";
|
|
26
26
|
import { isPreflightAcked, ackPreflight, getSessionOverrides } from "./preflight.js";
|
|
27
|
+
import { ensureBootstrap } from "../bootstrap.js";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Resolve the user's project directory.
|
|
@@ -118,6 +119,11 @@ const ERROR_CLASSIFIERS = [
|
|
|
118
119
|
test: (lower) => lower.includes("not a git repository"),
|
|
119
120
|
category: "git_error",
|
|
120
121
|
suggestion: "Current directory is not a git repository. Navigate to your project root or initialize git with 'git init'."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
test: (lower) => lower.includes("bootstrap failed"),
|
|
125
|
+
category: "bootstrap_error",
|
|
126
|
+
suggestion: "Environment prerequisites not met. Run kj_doctor for diagnostics, then fix the issues listed. Do NOT work around these — fix them properly."
|
|
121
127
|
}
|
|
122
128
|
];
|
|
123
129
|
|
|
@@ -149,6 +155,16 @@ export async function assertNotOnBaseBranch(config) {
|
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Run bootstrap gate: validate all environment prerequisites before execution.
|
|
160
|
+
* Throws if any prerequisite fails.
|
|
161
|
+
*/
|
|
162
|
+
async function runBootstrapGate(server, a) {
|
|
163
|
+
const projectDir = await resolveProjectDir(server, a.projectDir);
|
|
164
|
+
const { config } = await loadConfig(projectDir);
|
|
165
|
+
await ensureBootstrap(projectDir, config);
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
export function enrichedFailPayload(error, toolName) {
|
|
153
169
|
const msg = error?.message || String(error);
|
|
154
170
|
const { category, suggestion } = classifyError(error);
|
|
@@ -191,7 +207,7 @@ export function buildAskQuestion(server) {
|
|
|
191
207
|
|
|
192
208
|
const MAX_AUTO_RESUMES = 2;
|
|
193
209
|
const NON_RECOVERABLE_CATEGORIES = new Set([
|
|
194
|
-
"config_error", "auth_error", "agent_missing", "branch_error", "git_error"
|
|
210
|
+
"config_error", "auth_error", "agent_missing", "branch_error", "git_error", "bootstrap_error"
|
|
195
211
|
]);
|
|
196
212
|
|
|
197
213
|
async function attemptAutoResume({ err, config, logger, emitter, askQuestion, runLog }) {
|
|
@@ -783,6 +799,7 @@ async function handleResume(a, server, extra) {
|
|
|
783
799
|
if (!a.sessionId) {
|
|
784
800
|
return failPayload("Missing required field: sessionId");
|
|
785
801
|
}
|
|
802
|
+
await runBootstrapGate(server, a);
|
|
786
803
|
if (a.answer) {
|
|
787
804
|
const validation = validateResumeAnswer(a.answer);
|
|
788
805
|
if (!validation.valid) {
|
|
@@ -804,6 +821,7 @@ async function handleRun(a, server, extra) {
|
|
|
804
821
|
return failPayload(`Invalid taskType "${a.taskType}". Valid values: ${[...validTypes].join(", ")}`);
|
|
805
822
|
}
|
|
806
823
|
}
|
|
824
|
+
await runBootstrapGate(server, a);
|
|
807
825
|
if (!isPreflightAcked()) {
|
|
808
826
|
// Auto-acknowledge with defaults for autonomous operation
|
|
809
827
|
ackPreflight({});
|
|
@@ -818,6 +836,7 @@ async function handleCode(a, server, extra) {
|
|
|
818
836
|
if (!a.task) {
|
|
819
837
|
return failPayload("Missing required field: task");
|
|
820
838
|
}
|
|
839
|
+
await runBootstrapGate(server, a);
|
|
821
840
|
if (!isPreflightAcked()) {
|
|
822
841
|
// Auto-acknowledge with defaults for autonomous operation
|
|
823
842
|
ackPreflight({});
|
|
@@ -832,6 +851,7 @@ async function handleReview(a, server, extra) {
|
|
|
832
851
|
if (!a.task) {
|
|
833
852
|
return failPayload("Missing required field: task");
|
|
834
853
|
}
|
|
854
|
+
await runBootstrapGate(server, a);
|
|
835
855
|
return handleReviewDirect(a, server, extra);
|
|
836
856
|
}
|
|
837
857
|
|
|
@@ -839,6 +859,7 @@ async function handlePlan(a, server, extra) {
|
|
|
839
859
|
if (!a.task) {
|
|
840
860
|
return failPayload("Missing required field: task");
|
|
841
861
|
}
|
|
862
|
+
await runBootstrapGate(server, a);
|
|
842
863
|
return handlePlanDirect(a, server, extra);
|
|
843
864
|
}
|
|
844
865
|
|
|
@@ -850,6 +871,7 @@ async function handleDiscover(a, server, extra) {
|
|
|
850
871
|
if (a.mode && !validModes.has(a.mode)) {
|
|
851
872
|
return failPayload(`Invalid mode "${a.mode}". Valid values: ${[...validModes].join(", ")}`);
|
|
852
873
|
}
|
|
874
|
+
await runBootstrapGate(server, a);
|
|
853
875
|
return handleDiscoverDirect(a, server, extra);
|
|
854
876
|
}
|
|
855
877
|
|
|
@@ -857,6 +879,7 @@ async function handleTriage(a, server, extra) {
|
|
|
857
879
|
if (!a.task) {
|
|
858
880
|
return failPayload("Missing required field: task");
|
|
859
881
|
}
|
|
882
|
+
await runBootstrapGate(server, a);
|
|
860
883
|
return handleTriageDirect(a, server, extra);
|
|
861
884
|
}
|
|
862
885
|
|
|
@@ -864,6 +887,7 @@ async function handleResearcher(a, server, extra) {
|
|
|
864
887
|
if (!a.task) {
|
|
865
888
|
return failPayload("Missing required field: task");
|
|
866
889
|
}
|
|
890
|
+
await runBootstrapGate(server, a);
|
|
867
891
|
return handleResearcherDirect(a, server, extra);
|
|
868
892
|
}
|
|
869
893
|
|
|
@@ -871,10 +895,12 @@ async function handleArchitect(a, server, extra) {
|
|
|
871
895
|
if (!a.task) {
|
|
872
896
|
return failPayload("Missing required field: task");
|
|
873
897
|
}
|
|
898
|
+
await runBootstrapGate(server, a);
|
|
874
899
|
return handleArchitectDirect(a, server, extra);
|
|
875
900
|
}
|
|
876
901
|
|
|
877
902
|
async function handleAudit(a, server, extra) {
|
|
903
|
+
await runBootstrapGate(server, a);
|
|
878
904
|
return handleAuditDirect(a, server, extra);
|
|
879
905
|
}
|
|
880
906
|
|
|
@@ -892,6 +918,11 @@ async function handleBoard(a) {
|
|
|
892
918
|
}
|
|
893
919
|
}
|
|
894
920
|
|
|
921
|
+
async function handleScan(a, server) {
|
|
922
|
+
await runBootstrapGate(server, a);
|
|
923
|
+
return runKjCommand({ command: "scan", options: a });
|
|
924
|
+
}
|
|
925
|
+
|
|
895
926
|
/* ── Handler dispatch map ─────────────────────────────────────────── */
|
|
896
927
|
|
|
897
928
|
const toolHandlers = {
|
|
@@ -901,7 +932,7 @@ const toolHandlers = {
|
|
|
901
932
|
kj_agents: (a) => handleAgents(a),
|
|
902
933
|
kj_preflight: (a) => handlePreflight(a),
|
|
903
934
|
kj_config: (a) => runKjCommand({ command: "config", commandArgs: a.json ? ["--json"] : [], options: a }),
|
|
904
|
-
kj_scan: (a) =>
|
|
935
|
+
kj_scan: (a, server) => handleScan(a, server),
|
|
905
936
|
kj_roles: (a) => handleRoles(a),
|
|
906
937
|
kj_report: (a) => runKjCommand({ command: "report", commandArgs: buildReportArgs(a), options: a }),
|
|
907
938
|
kj_resume: (a, server, extra) => handleResume(a, server, extra),
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* Preflight environment checks for kj_run.
|
|
3
3
|
*
|
|
4
4
|
* Runs AFTER policy resolution (so we know which stages are active)
|
|
5
|
-
* and BEFORE session iteration loop (so we fail fast
|
|
5
|
+
* and BEFORE session iteration loop (so we fail fast).
|
|
6
6
|
*
|
|
7
|
-
* Design:
|
|
8
|
-
*
|
|
7
|
+
* Design: SonarQube checks are BLOCKING when enabled — if SonarQube is
|
|
8
|
+
* configured but not available, the pipeline STOPS with a clear error.
|
|
9
|
+
* Security agent checks remain graceful (warning, auto-disable).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
12
13
|
import { isSonarReachable, sonarUp } from "../sonar/manager.js";
|
|
13
14
|
import { runCommand } from "../utils/process.js";
|
|
14
15
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
+
import { loadSonarCredentials } from "../sonar/credentials.js";
|
|
15
17
|
|
|
16
18
|
function normalizeApiHost(rawHost) {
|
|
17
19
|
return String(rawHost || "http://localhost:9000").replace(/host\.docker\.internal/g, "localhost");
|
|
@@ -75,14 +77,19 @@ async function checkSonarAuth(config) {
|
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
// Try admin credentials
|
|
79
|
-
const
|
|
80
|
+
// Try admin credentials: env vars → config → ~/.karajan/sonar-credentials.json (NO default admin/admin)
|
|
81
|
+
const fileCreds = await loadSonarCredentials() || {};
|
|
82
|
+
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube?.admin_user || fileCreds.user;
|
|
80
83
|
const candidates = [
|
|
81
84
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
82
85
|
config.sonarqube?.admin_password,
|
|
83
|
-
|
|
86
|
+
fileCreds.password
|
|
84
87
|
].filter(Boolean);
|
|
85
88
|
|
|
89
|
+
if (!adminUser || candidates.length === 0) {
|
|
90
|
+
return { name: "sonar-auth", ok: false, detail: "No Sonar token or admin credentials configured. Set KJ_SONAR_TOKEN, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json." };
|
|
91
|
+
}
|
|
92
|
+
|
|
86
93
|
for (const password of [...new Set(candidates)]) {
|
|
87
94
|
const validateRes = await runCommand("curl", [
|
|
88
95
|
"-sS", "-u", `${adminUser}:${password}`,
|
|
@@ -128,6 +135,10 @@ async function checkSecurityAgent(config) {
|
|
|
128
135
|
/**
|
|
129
136
|
* Run preflight environment checks.
|
|
130
137
|
*
|
|
138
|
+
* SonarQube checks are BLOCKING: if SonarQube is enabled but not available,
|
|
139
|
+
* ok will be false and errors[] will contain actionable fix instructions.
|
|
140
|
+
* Security agent checks remain graceful (auto-disable via configOverrides).
|
|
141
|
+
*
|
|
131
142
|
* @param {object} opts
|
|
132
143
|
* @param {object} opts.config - Karajan config
|
|
133
144
|
* @param {object} opts.logger - Logger instance
|
|
@@ -135,7 +146,7 @@ async function checkSecurityAgent(config) {
|
|
|
135
146
|
* @param {object} opts.eventBase - Base event data
|
|
136
147
|
* @param {object} opts.resolvedPolicies - Output from applyPolicies()
|
|
137
148
|
* @param {boolean} opts.securityEnabled - Whether security stage is enabled
|
|
138
|
-
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[] }}
|
|
149
|
+
* @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[], errors: object[] }}
|
|
139
150
|
*/
|
|
140
151
|
export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
|
|
141
152
|
const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
|
|
@@ -148,6 +159,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
148
159
|
remediations: [],
|
|
149
160
|
configOverrides: {},
|
|
150
161
|
warnings: [],
|
|
162
|
+
errors: [],
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
// Short-circuit: nothing to check
|
|
@@ -171,20 +183,24 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
171
183
|
result.checks.push(dockerCheck);
|
|
172
184
|
|
|
173
185
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
174
|
-
status: dockerCheck.ok ? "ok" : "
|
|
186
|
+
status: dockerCheck.ok ? "ok" : "fail",
|
|
175
187
|
message: `Docker: ${dockerCheck.detail}`,
|
|
176
188
|
detail: dockerCheck
|
|
177
189
|
}));
|
|
178
190
|
|
|
179
191
|
if (!dockerCheck.ok) {
|
|
180
|
-
result.
|
|
181
|
-
result.
|
|
182
|
-
|
|
192
|
+
result.ok = false;
|
|
193
|
+
result.errors.push({
|
|
194
|
+
check: "docker",
|
|
195
|
+
message: "Docker not available but SonarQube is enabled.",
|
|
196
|
+
fix: "Start Docker, or disable SonarQube: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
197
|
+
});
|
|
198
|
+
logger.error("Preflight: Docker not found — SonarQube requires Docker");
|
|
183
199
|
|
|
184
200
|
// Skip remaining sonar checks, continue to security
|
|
185
201
|
if (!securityEnabled) {
|
|
186
202
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
187
|
-
status: "
|
|
203
|
+
status: "fail", message: "Preflight FAILED — environment not ready", detail: result
|
|
188
204
|
}));
|
|
189
205
|
return result;
|
|
190
206
|
}
|
|
@@ -192,7 +208,7 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
// --- 2. SonarQube reachable ---
|
|
195
|
-
if (sonarEnabled &&
|
|
211
|
+
if (sonarEnabled && result.ok) {
|
|
196
212
|
const reachableCheck = await checkSonarReachable(sonarHost);
|
|
197
213
|
result.checks.push(reachableCheck);
|
|
198
214
|
|
|
@@ -201,25 +217,29 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
204
|
-
status: reachableCheck.ok ? "ok" : "
|
|
220
|
+
status: reachableCheck.ok ? "ok" : "fail",
|
|
205
221
|
message: `SonarQube reachability: ${reachableCheck.detail}`,
|
|
206
222
|
detail: reachableCheck
|
|
207
223
|
}));
|
|
208
224
|
|
|
209
225
|
if (!reachableCheck.ok) {
|
|
210
|
-
result.
|
|
211
|
-
result.
|
|
212
|
-
|
|
226
|
+
result.ok = false;
|
|
227
|
+
result.errors.push({
|
|
228
|
+
check: "sonar-reachable",
|
|
229
|
+
message: `SonarQube not reachable at ${sonarHost}.`,
|
|
230
|
+
fix: "Start SonarQube: 'docker start karajan-sonarqube', or disable it: set sonarqube.enabled: false in kj.config.yml, or pass --no-sonar."
|
|
231
|
+
});
|
|
232
|
+
logger.error("Preflight: SonarQube not reachable after remediation attempt");
|
|
213
233
|
}
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
// --- 3. SonarQube auth/token ---
|
|
217
|
-
if (sonarEnabled &&
|
|
237
|
+
if (sonarEnabled && result.ok) {
|
|
218
238
|
const authCheck = await checkSonarAuth(config);
|
|
219
239
|
result.checks.push(authCheck);
|
|
220
240
|
|
|
221
241
|
emitProgress(emitter, makeEvent("preflight:check", { ...eventBase, stage: "preflight" }, {
|
|
222
|
-
status: authCheck.ok ? "ok" : "
|
|
242
|
+
status: authCheck.ok ? "ok" : "fail",
|
|
223
243
|
message: `SonarQube auth: ${authCheck.detail}`,
|
|
224
244
|
detail: { name: authCheck.name, ok: authCheck.ok, detail: authCheck.detail }
|
|
225
245
|
}));
|
|
@@ -229,13 +249,17 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
229
249
|
result.remediations.push("Sonar token resolved and cached in KJ_SONAR_TOKEN");
|
|
230
250
|
logger.info("Preflight: Sonar token resolved and cached");
|
|
231
251
|
} else if (!authCheck.ok) {
|
|
232
|
-
result.
|
|
233
|
-
result.
|
|
234
|
-
|
|
252
|
+
result.ok = false;
|
|
253
|
+
result.errors.push({
|
|
254
|
+
check: "sonar-auth",
|
|
255
|
+
message: "SonarQube authentication failed.",
|
|
256
|
+
fix: "Regenerate the SonarQube token and update it via kj_init or in kj.config.yml under sonarqube.token."
|
|
257
|
+
});
|
|
258
|
+
logger.error("Preflight: Sonar auth failed");
|
|
235
259
|
}
|
|
236
260
|
}
|
|
237
261
|
|
|
238
|
-
// --- 4. Security agent ---
|
|
262
|
+
// --- 4. Security agent (graceful — only warning, not blocking) ---
|
|
239
263
|
if (securityEnabled) {
|
|
240
264
|
const secCheck = await checkSecurityAgent(config);
|
|
241
265
|
result.checks.push(secCheck);
|
|
@@ -253,12 +277,15 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
|
|
|
253
277
|
}
|
|
254
278
|
}
|
|
255
279
|
|
|
280
|
+
const hasErrors = result.errors.length > 0;
|
|
256
281
|
const hasWarnings = result.warnings.length > 0;
|
|
257
282
|
emitProgress(emitter, makeEvent("preflight:end", { ...eventBase, stage: "preflight" }, {
|
|
258
|
-
status: hasWarnings ? "warn" : "ok",
|
|
259
|
-
message:
|
|
260
|
-
? `Preflight
|
|
261
|
-
:
|
|
283
|
+
status: hasErrors ? "fail" : hasWarnings ? "warn" : "ok",
|
|
284
|
+
message: hasErrors
|
|
285
|
+
? `Preflight FAILED — ${result.errors.length} blocking issue(s)`
|
|
286
|
+
: hasWarnings
|
|
287
|
+
? `Preflight completed with ${result.warnings.length} warning(s)`
|
|
288
|
+
: "Preflight passed — all checks OK",
|
|
262
289
|
detail: result
|
|
263
290
|
}));
|
|
264
291
|
|
package/src/orchestrator.js
CHANGED
|
@@ -886,9 +886,16 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
886
886
|
session.preflight = preflightResult;
|
|
887
887
|
await saveSession(session);
|
|
888
888
|
|
|
889
|
-
if (
|
|
890
|
-
|
|
889
|
+
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
890
|
+
if (!preflightResult.ok) {
|
|
891
|
+
const errorLines = (preflightResult.errors || [])
|
|
892
|
+
.map(e => ` - ${e.message}\n Fix: ${e.fix}`)
|
|
893
|
+
.join("\n");
|
|
894
|
+
throw new Error(
|
|
895
|
+
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
|
896
|
+
);
|
|
891
897
|
}
|
|
898
|
+
|
|
892
899
|
if (preflightResult.configOverrides.securityDisabled) {
|
|
893
900
|
pipelineFlags.securityEnabled = false;
|
|
894
901
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load SonarQube admin credentials from ~/.karajan/sonar-credentials.json.
|
|
3
|
+
*
|
|
4
|
+
* File format:
|
|
5
|
+
* {
|
|
6
|
+
* "user": "admin",
|
|
7
|
+
* "password": "your-password"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* Returns { user, password } or { user: null, password: null } if file missing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs/promises";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
16
|
+
|
|
17
|
+
const CREDENTIALS_FILENAME = "sonar-credentials.json";
|
|
18
|
+
|
|
19
|
+
export async function loadSonarCredentials() {
|
|
20
|
+
try {
|
|
21
|
+
const filePath = path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
22
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
23
|
+
const data = JSON.parse(raw);
|
|
24
|
+
return {
|
|
25
|
+
user: data.user || null,
|
|
26
|
+
password: data.password || null
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return { user: null, password: null };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function credentialsPath() {
|
|
34
|
+
return path.join(getKarajanHome(), CREDENTIALS_FILENAME);
|
|
35
|
+
}
|
package/src/sonar/scanner.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { runCommand } from "../utils/process.js";
|
|
5
5
|
import { sonarUp } from "./manager.js";
|
|
6
6
|
import { resolveSonarProjectKey } from "./project-key.js";
|
|
7
|
+
import { loadSonarCredentials } from "./credentials.js";
|
|
7
8
|
|
|
8
9
|
export function buildScannerOpts(projectKey, scanner = {}) {
|
|
9
10
|
const opts = [`-Dsonar.projectKey=${projectKey}`];
|
|
@@ -136,18 +137,18 @@ async function resolveSonarToken(config, apiHost) {
|
|
|
136
137
|
const explicitToken = process.env.KJ_SONAR_TOKEN || process.env.SONAR_TOKEN || config.sonarqube.token;
|
|
137
138
|
if (explicitToken) return explicitToken;
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
// Resolve admin credentials from: env vars → config → ~/.karajan/sonar-credentials.json
|
|
141
|
+
const fileCreds = await loadSonarCredentials() || {};
|
|
142
|
+
const adminUser = process.env.KJ_SONAR_ADMIN_USER || config.sonarqube.admin_user || fileCreds.user;
|
|
140
143
|
const candidates = [
|
|
141
144
|
process.env.KJ_SONAR_ADMIN_PASSWORD,
|
|
142
145
|
config.sonarqube.admin_password,
|
|
143
|
-
|
|
146
|
+
fileCreds.password
|
|
144
147
|
].filter(Boolean);
|
|
145
148
|
|
|
149
|
+
if (!adminUser || candidates.length === 0) return null;
|
|
150
|
+
|
|
146
151
|
for (const password of new Set(candidates)) {
|
|
147
|
-
if (password === "admin") {
|
|
148
|
-
// eslint-disable-next-line no-console
|
|
149
|
-
console.warn("[karajan] WARNING: Using default admin/admin credentials for SonarQube. Set KJ_SONAR_TOKEN for production use.");
|
|
150
|
-
}
|
|
151
152
|
const valid = await validateAdminCredentials(apiHost, adminUser, password);
|
|
152
153
|
if (!valid) continue;
|
|
153
154
|
const token = await generateUserToken(apiHost, adminUser, password);
|
|
@@ -224,7 +225,7 @@ export async function runSonarScan(config, projectKey = null) {
|
|
|
224
225
|
ok: false,
|
|
225
226
|
stdout: "",
|
|
226
227
|
stderr:
|
|
227
|
-
"Unable to resolve Sonar token.
|
|
228
|
+
"Unable to resolve Sonar token. Set KJ_SONAR_TOKEN env var, configure sonarqube.token in kj.config.yml, or save credentials in ~/.karajan/sonar-credentials.json.",
|
|
228
229
|
exitCode: 1
|
|
229
230
|
};
|
|
230
231
|
}
|