karajan-code 4.19.0 → 4.20.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/package.json +1 -1
- package/src/agents/base-agent.js +14 -4
- package/src/agents/codex-agent.js +18 -1
- package/src/audit/deterministic-summary.js +21 -0
- package/src/audit/infra-findings.js +100 -0
- package/src/commands/start.js +6 -2
- package/src/config/role-resolver.js +23 -13
- package/src/harden/sentinel-hooks.js +50 -11
- package/src/roles/audit-role.js +12 -1
- package/src/roles/tester-role.js +20 -3
- package/src/start/sweep.js +3 -0
- package/src/utils/project-detect.js +37 -3
package/package.json
CHANGED
package/src/agents/base-agent.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { defaultEnvironment } from "../infrastructure/environment.js";
|
|
14
14
|
import { buildAgentEnv } from "../utils/role-env.js";
|
|
15
|
+
import { isModelCompatible } from "../config/role-resolver.js";
|
|
15
16
|
|
|
16
17
|
const MODEL_NOT_SUPPORTED_PATTERNS = [
|
|
17
18
|
/model.{0,30}is not supported/i,
|
|
@@ -95,10 +96,19 @@ export class BaseAgent {
|
|
|
95
96
|
* @returns {string|null}
|
|
96
97
|
*/
|
|
97
98
|
getRoleModel(role) {
|
|
98
|
-
const roleModel =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
const roleModel =
|
|
100
|
+
this.config?.roles?.[role]?.model ||
|
|
101
|
+
(role === "reviewer" ? this.config?.reviewer_options?.model : this.config?.coder_options?.model) ||
|
|
102
|
+
null;
|
|
103
|
+
// KJC-BUG-0144 (campo, issue #1465): un default por-rol (start: haiku)
|
|
104
|
+
// puede caer sobre un provider de OTRA familia (agy, codex...) — el
|
|
105
|
+
// modelo se descarta con aviso y el agente usa su propio default,
|
|
106
|
+
// nunca se le pasa un --model ajeno.
|
|
107
|
+
if (roleModel && !isModelCompatible(this.name, roleModel)) {
|
|
108
|
+
this.logger?.warn?.(`model "${roleModel}" belongs to another family — dropping it for ${this.name} (role ${role})`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return roleModel;
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/**
|
|
@@ -77,9 +77,26 @@ export class CodexAgent extends BaseAgent {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
async _exec(task, model, role) {
|
|
80
|
+
// KJC-BUG-0143 (campo, issue #1471): codex exec >= 0.146 rechaza
|
|
81
|
+
// --full-auto ("unexpected argument"); el auto-approve moderno para un
|
|
82
|
+
// coder que escribe es --sandbox workspace-write. Los CLIs antiguos no
|
|
83
|
+
// conocen --sandbox: si lo rechazan, se reintenta con el flag legacy.
|
|
84
|
+
const autoApprove = role !== "reviewer" && this.isAutoApproveEnabled(role);
|
|
85
|
+
const res = await this._execOnce(task, model, role, { autoApprove, legacyAutoFlag: false });
|
|
86
|
+
if (!res.ok && autoApprove && /unexpected argument '--sandbox'/.test(res.error || "")) {
|
|
87
|
+
this.logger?.warn("codex CLI sin --sandbox — reintentando con el flag legacy --full-auto");
|
|
88
|
+
return this._execOnce(task, model, role, { autoApprove, legacyAutoFlag: true });
|
|
89
|
+
}
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _execOnce(task, model, role, { autoApprove, legacyAutoFlag }) {
|
|
80
94
|
const args = ["exec", "--skip-git-repo-check"];
|
|
81
95
|
if (model) args.push("--model", model);
|
|
82
|
-
if (
|
|
96
|
+
if (autoApprove) {
|
|
97
|
+
if (legacyAutoFlag) args.push("--full-auto");
|
|
98
|
+
else args.push("--sandbox", "workspace-write");
|
|
99
|
+
}
|
|
83
100
|
args.push("-");
|
|
84
101
|
const res = await this.runCommand(resolveBin("codex"), args, {
|
|
85
102
|
onOutput: task.onOutput,
|
|
@@ -38,6 +38,7 @@ export function formatDeterministicSummary(ctx) {
|
|
|
38
38
|
if (ctx.sonarFindings) lines.push(...formatSonarBlock(ctx.sonarFindings));
|
|
39
39
|
if (ctx.osvFindings) lines.push(...formatOsvBlock(ctx.osvFindings));
|
|
40
40
|
if (ctx.semgrepFindings) lines.push(...formatSemgrepBlock(ctx.semgrepFindings));
|
|
41
|
+
if (ctx.infraFindings) lines.push(...formatInfraBlock(ctx.infraFindings));
|
|
41
42
|
if (ctx.circularDeps) lines.push(...formatCircularDepsBlock(ctx.circularDeps));
|
|
42
43
|
if (ctx.deadExports) lines.push(...formatDeadExportsBlock(ctx.deadExports));
|
|
43
44
|
if (ctx.injectionFindings) lines.push(...formatInjectionBlock(ctx.injectionFindings));
|
|
@@ -133,6 +134,25 @@ function formatSemgrepBlock(semgrepFindings) {
|
|
|
133
134
|
return lines;
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// INF-C (KJC-TSK-0760): checkov misconfigs — same shape as the semgrep
|
|
138
|
+
// block; a not-applicable repo (no infra markers) declares it in one line.
|
|
139
|
+
function formatInfraBlock(infraFindings) {
|
|
140
|
+
if (!infraFindings.available) {
|
|
141
|
+
return ["### Infra misconfigurations (checkov)", `- Status: not available — ${infraFindings.reason || "checkov not installed"}`, ""];
|
|
142
|
+
}
|
|
143
|
+
const lines = ["### Infra misconfigurations (checkov)"];
|
|
144
|
+
lines.push(`- Total findings: ${infraFindings.total ?? 0}`);
|
|
145
|
+
const cap = MAX_SAMPLE_SEMGREP_PER_SEVERITY * 3;
|
|
146
|
+
for (const f of (infraFindings.findings || []).slice(0, cap)) {
|
|
147
|
+
const lineSuffix = f.line ? `:${f.line}` : "";
|
|
148
|
+
lines.push(` - [${f.severity}] ${f.file}${lineSuffix} [${f.rule}] ${f.message}${f.guideline ? ` (${f.guideline})` : ""}`);
|
|
149
|
+
}
|
|
150
|
+
const overflow = (infraFindings.total ?? 0) - cap;
|
|
151
|
+
if (overflow > 0) lines.push(` - ... and ${overflow} more`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
function formatOsvBlock(osvFindings) {
|
|
137
157
|
if (!osvFindings.available) {
|
|
138
158
|
return ["### OSV Vulnerabilities", `- Status: not available — ${osvFindings.reason || "osv-scanner not installed"}`, ""];
|
|
@@ -298,6 +318,7 @@ export function deterministicContextHasFindings(ctx) {
|
|
|
298
318
|
if (ctx.sonarFindings?.qualityGate?.status === "ERROR") return true;
|
|
299
319
|
if (ctx.osvFindings?.available && (ctx.osvFindings.total ?? 0) > 0) return true;
|
|
300
320
|
if (ctx.semgrepFindings?.available && (ctx.semgrepFindings.total ?? 0) > 0) return true;
|
|
321
|
+
if (ctx.infraFindings?.available && (ctx.infraFindings.total ?? 0) > 0) return true;
|
|
301
322
|
if (ctx.circularDeps?.available && (ctx.circularDeps.total ?? 0) > 0) return true;
|
|
302
323
|
if (ctx.deadExports?.available && (ctx.deadExports.total ?? 0) > 0) return true;
|
|
303
324
|
if (ctx.growthDelta && (Math.abs(ctx.growthDelta.lines || 0) > 100 || Math.abs(ctx.growthDelta.deps || 0) > 0)) return true;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkov infra-misconfiguration collector for `kj audit` (INF-C,
|
|
3
|
+
* KJC-TSK-0760, epic KJC-PCS-0078).
|
|
4
|
+
*
|
|
5
|
+
* One tool covers the whole infra surface — terraform, kubernetes, helm,
|
|
6
|
+
* kustomize, ansible, dockerfiles — so checkov is THE infra scanner (tfsec
|
|
7
|
+
* and tflint stay out of scope: single dependency to learn and install).
|
|
8
|
+
* Findings enter the audit through the SAME best-effort channel as
|
|
9
|
+
* semgrep-findings (KJC-TSK-0366): no infra markers → not-applicable (zero
|
|
10
|
+
* noise, checkov never runs); missing binary → declared with the install
|
|
11
|
+
* command; non-zero exit with JSON on stdout (checkov's failed-checks
|
|
12
|
+
* behavior) rescued; unparseable output degrades SAYING so.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { runGoverned, jobsCap } from "../utils/tool-governor.js";
|
|
16
|
+
import { detectInfraMarkers } from "../utils/project-detect.js";
|
|
17
|
+
|
|
18
|
+
const SCAN_TIMEOUT_MS = 120_000;
|
|
19
|
+
const INSTALL_HINT = "install via 'pipx install checkov' or 'pip install checkov'";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run checkov on the project and return a structured result.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} projectDir - absolute path to the project root
|
|
25
|
+
* @param {object} [logger]
|
|
26
|
+
* @returns {Promise<{available: boolean, reason?: string, total?: number, findings?: object[]}>}
|
|
27
|
+
*/
|
|
28
|
+
export async function collectInfraFindings(projectDir, logger = null) {
|
|
29
|
+
// KJC-BUG-0122 doctrine: external scanners never fire from the unit suite.
|
|
30
|
+
if (process.env.VITEST && process.env.KJ_ALLOW_REAL_SCANS !== "1") {
|
|
31
|
+
return { available: false, reason: "real scans disabled under the test runner (set KJ_ALLOW_REAL_SCANS=1 to opt in)" };
|
|
32
|
+
}
|
|
33
|
+
if (!(await detectInfraMarkers(projectDir))) {
|
|
34
|
+
return { available: false, reason: "no infra markers (terraform/helm/kustomize/ansible) — not applicable" };
|
|
35
|
+
}
|
|
36
|
+
let stdout;
|
|
37
|
+
try {
|
|
38
|
+
const { stdout: out } = await runGoverned(
|
|
39
|
+
"checkov",
|
|
40
|
+
["-d", projectDir || ".", "--output", "json", "--quiet", "--compact", `--parallel-runner-count=${jobsCap()}`],
|
|
41
|
+
{ cwd: projectDir, timeout: SCAN_TIMEOUT_MS + 5000, maxBuffer: 32 * 1024 * 1024 }
|
|
42
|
+
);
|
|
43
|
+
stdout = out;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === "ENOENT") {
|
|
46
|
+
if (logger?.warn) logger.warn(`checkov not found in PATH — ${INSTALL_HINT}`);
|
|
47
|
+
return { available: false, reason: `checkov not installed — ${INSTALL_HINT}` };
|
|
48
|
+
}
|
|
49
|
+
if (err.killed) {
|
|
50
|
+
if (logger?.warn) logger.warn(`checkov timed out after ${SCAN_TIMEOUT_MS}ms`);
|
|
51
|
+
return { available: false, reason: `checkov timed out after ${SCAN_TIMEOUT_MS}ms` };
|
|
52
|
+
}
|
|
53
|
+
// checkov exits non-zero when checks fail, but still emits the report.
|
|
54
|
+
if (typeof err.stdout === "string" && err.stdout.trim().length > 0) {
|
|
55
|
+
stdout = err.stdout;
|
|
56
|
+
} else {
|
|
57
|
+
if (logger?.warn) logger.warn(`checkov failed: ${err.message}`);
|
|
58
|
+
return { available: false, reason: `checkov failed: ${err.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(stdout);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (logger?.warn) logger.warn(`checkov output not parseable: ${err.message}`);
|
|
67
|
+
return { available: false, reason: "checkov output not parseable as JSON" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { available: true, ...parseCheckovOutput(parsed) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert checkov's JSON (object for one framework, array for several)
|
|
75
|
+
* into the flat findings list the audit prompt consumes. Checkov OSS may
|
|
76
|
+
* omit severity — WARNING is the honest default.
|
|
77
|
+
*
|
|
78
|
+
* @param {object|object[]} raw
|
|
79
|
+
* @returns {{total: number, findings: object[]}}
|
|
80
|
+
*/
|
|
81
|
+
export function parseCheckovOutput(raw) {
|
|
82
|
+
const reports = Array.isArray(raw) ? raw : [raw];
|
|
83
|
+
const findings = [];
|
|
84
|
+
for (const report of reports) {
|
|
85
|
+
const failed = report?.results?.failed_checks;
|
|
86
|
+
if (!Array.isArray(failed)) continue;
|
|
87
|
+
for (const c of failed) {
|
|
88
|
+
findings.push({
|
|
89
|
+
rule: c.check_id || "(unknown)",
|
|
90
|
+
severity: String(c.severity || "WARNING").toUpperCase(),
|
|
91
|
+
framework: report.check_type || "infra",
|
|
92
|
+
file: String(c.file_path || "").replace(/^\//, ""),
|
|
93
|
+
line: Array.isArray(c.file_line_range) ? (c.file_line_range[0] ?? 0) : 0,
|
|
94
|
+
message: c.check_name || "",
|
|
95
|
+
guideline: c.guideline || null,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { total: findings.length, findings };
|
|
100
|
+
}
|
package/src/commands/start.js
CHANGED
|
@@ -80,7 +80,11 @@ export async function startCommand({ task = "", config, logger, flags = {}, deps
|
|
|
80
80
|
const spawn = deps.spawnKj || spawnKj;
|
|
81
81
|
const makeWizard = deps.makeWizard || (() => createWizard());
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// KJC-BUG-0145 (campo, #1471): loadConfig never sets projectDir — passing
|
|
84
|
+
// config.projectDir straight through handed `undefined` to the sweep, every
|
|
85
|
+
// collector failed silently and the project read as "new, no source code".
|
|
86
|
+
const projectDir = config.projectDir || process.cwd();
|
|
87
|
+
const bundle = await sweep(projectDir, { declared: flags.maturity || null });
|
|
84
88
|
const { text, summary } = assess(bundle);
|
|
85
89
|
let result = await decide({ task, text, config, logger, makeDecider });
|
|
86
90
|
|
|
@@ -111,7 +115,7 @@ export async function startCommand({ task = "", config, logger, flags = {}, deps
|
|
|
111
115
|
// Dispatch the chosen intent to its saved command, confirming first (it writes).
|
|
112
116
|
const toArgs = DISPATCH[result.intent];
|
|
113
117
|
if (interactive && toArgs && (await confirmApply(makeWizard))) {
|
|
114
|
-
await spawn(toArgs(goal), { cwd:
|
|
118
|
+
await spawn(toArgs(goal), { cwd: projectDir });
|
|
115
119
|
}
|
|
116
120
|
return { ok: true, intent: result.intent };
|
|
117
121
|
}
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Check if a model string is compatible with an agent provider.
|
|
23
|
-
* Only returns false when the model clearly belongs to a DIFFERENT
|
|
23
|
+
* Only returns false when the model clearly belongs to a DIFFERENT family.
|
|
24
24
|
* Returns true if we can't determine or if the model is ambiguous.
|
|
25
|
+
*
|
|
26
|
+
* KJC-BUG-0144: exported and family-based — an agent may serve another
|
|
27
|
+
* family's models (agy is Google's Antigravity, gemini family), and the
|
|
28
|
+
* same check now guards BaseAgent.getRoleModel so per-role defaults
|
|
29
|
+
* (e.g. start's haiku) never reach a provider from another family.
|
|
25
30
|
*/
|
|
26
31
|
const AGENT_MODEL_SIGNATURES = {
|
|
27
32
|
claude: ["claude", "sonnet", "opus", "haiku"],
|
|
@@ -29,21 +34,26 @@ const AGENT_MODEL_SIGNATURES = {
|
|
|
29
34
|
gemini: ["gemini", "flash-"]
|
|
30
35
|
};
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// Single-family CLIs: they only serve their own vendor's models. Agents NOT
|
|
38
|
+
// in this map (aider, opencode, copilot, kimi...) are multi-model hosts —
|
|
39
|
+
// they route to arbitrary models, so any model name is allowed there.
|
|
40
|
+
const AGENT_FAMILY = { claude: "claude", codex: "codex", gemini: "gemini", agy: "gemini" };
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (signatures.some(s => lower.includes(s)))
|
|
40
|
-
// Model belongs to a different provider — incompatible
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
42
|
+
function modelFamily(model) {
|
|
43
|
+
const lower = model.toLowerCase();
|
|
44
|
+
for (const [family, signatures] of Object.entries(AGENT_MODEL_SIGNATURES)) {
|
|
45
|
+
if (signatures.some(s => lower.includes(s))) return family;
|
|
43
46
|
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
return true;
|
|
50
|
+
export function isModelCompatible(agent, model) {
|
|
51
|
+
if (!model || !agent) return true;
|
|
52
|
+
const agentFamily = AGENT_FAMILY[agent];
|
|
53
|
+
if (!agentFamily) return true; // multi-model host — bring your own model
|
|
54
|
+
const family = modelFamily(model);
|
|
55
|
+
if (!family) return true; // ambiguous/custom/local — allow it
|
|
56
|
+
return family === agentFamily;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
// Roles that inherit provider/model from the coder when not explicitly configured
|
|
@@ -35,8 +35,10 @@ import { fileURLToPath } from "node:url";
|
|
|
35
35
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
36
|
export const STATE = join(here, "sentinel-state.json");
|
|
37
37
|
export const ROOT = join(here, "..", "..");
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// INF-A (KJC-TSK-0758): infra IS source — terraform/k8s/shell/Docker edits
|
|
39
|
+
// engage the method like app code; *_test.* files (terratest) are tests.
|
|
40
|
+
export const TESTS = /(^|\\/)(tests?|__tests__|spec)\\/|\\.(test|spec)\\.[a-z]+$|_test\\.[a-z]+$/;
|
|
41
|
+
export const CODE = /\\.(m?[jt]sx?|c[jt]s|py|go|rs|java|rb|php|cs|swift|kt|astro|svelte|vue|c|h|cc|cpp|hpp|tf|hcl|ya?ml|sh)$|(^|\\/)(Dockerfile|Makefile|Vagrantfile)$/;
|
|
40
42
|
export const CARD = new RegExp(${JSON.stringify(CARD_REF_RE.source)}, "i");
|
|
41
43
|
export const BASE_BRANCHES = new Set(["main", "master"]);
|
|
42
44
|
export const load = () => { try { return JSON.parse(readFileSync(STATE, "utf8")); } catch { return {}; } };
|
|
@@ -226,6 +228,43 @@ process.stdin.on("end", () => {
|
|
|
226
228
|
process.exit(2);
|
|
227
229
|
}
|
|
228
230
|
if (process.env.KJ_SENTINEL_OFF === "1") process.exit(0);
|
|
231
|
+
// KJC-BUG-0142: cada deny anuncia su escape como prefijo del comando
|
|
232
|
+
// (KJ_ALLOW_X=1 cmd), pero el hook corre con el env del HOST — el
|
|
233
|
+
// prefijo jamas llegaba a process.env y el escape era inalcanzable
|
|
234
|
+
// (deadlock real del gate de release: la landing solo se pone verde
|
|
235
|
+
// tras el deploy y el deploy estaba bloqueado por el mismo gate). Un
|
|
236
|
+
// escape es una excepcion CONSCIENTE y auditada, no un boundary de
|
|
237
|
+
// seguridad (security sigue sin escape y la autoprotecion corre antes),
|
|
238
|
+
// asi que el TEXTO del comando es fuente valida: solo asignaciones
|
|
239
|
+
// LITERALES al inicio (tokens VAR=VAL antes del primer verbo) y solo
|
|
240
|
+
// sobre un comando SIMPLE — con ;|& $() backtick o salto de linea el
|
|
241
|
+
// prefijo shell no alcanzaria a los comandos posteriores (catch de
|
|
242
|
+
// codex: "X=1 true && npm publish" escaparia sin serlo), asi que el
|
|
243
|
+
// escape por texto se rechaza y queda la via env de siempre.
|
|
244
|
+
const CMD_TEXT = tool === "Bash" ? String(input.command || "") : "";
|
|
245
|
+
const ESC_TAB = String.fromCharCode(9);
|
|
246
|
+
const ESC_NL = String.fromCharCode(10);
|
|
247
|
+
const ESC_BT = String.fromCharCode(96);
|
|
248
|
+
const escOn = (name) => {
|
|
249
|
+
if (process.env[name] === "1") return true;
|
|
250
|
+
let found = false;
|
|
251
|
+
let i = 0;
|
|
252
|
+
while (i < CMD_TEXT.length) {
|
|
253
|
+
while (i < CMD_TEXT.length && (CMD_TEXT[i] === " " || CMD_TEXT[i] === ESC_TAB)) i += 1;
|
|
254
|
+
const start = i;
|
|
255
|
+
while (i < CMD_TEXT.length && CMD_TEXT[i] !== " " && CMD_TEXT[i] !== ESC_TAB && CMD_TEXT[i] !== ESC_NL) i += 1;
|
|
256
|
+
const tok = CMD_TEXT.slice(start, i);
|
|
257
|
+
const eq = tok.indexOf("=");
|
|
258
|
+
if (eq <= 0 || !/^[A-Z][A-Z0-9_]*$/.test(tok.slice(0, eq))) break;
|
|
259
|
+
if (tok === name + "=1") found = true;
|
|
260
|
+
}
|
|
261
|
+
if (!found) return false;
|
|
262
|
+
for (const c of CMD_TEXT) {
|
|
263
|
+
if (c === ";" || c === "|" || c === "&" || c === ESC_NL || c === ESC_BT) return false;
|
|
264
|
+
if (c === "$" || c === "(" || c === ")") return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
229
268
|
// MONO-0 (KJC-TSK-0737, ADR 0002): cada sesion muta solo SU worktree;
|
|
230
269
|
// otro carril del MISMO repo se deniega ANTES del dano. Lectura libre.
|
|
231
270
|
const laneOf = (p) => {
|
|
@@ -243,7 +282,7 @@ process.stdin.on("end", () => {
|
|
|
243
282
|
return d ? foreignLane(d) : null;
|
|
244
283
|
};
|
|
245
284
|
const laneDeny = (what, lane) => {
|
|
246
|
-
if (
|
|
285
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); return false; }
|
|
247
286
|
console.error("kj sentinel: " + what + " vive en otro carril (" + lane + ") de este repo — cada sesion muta solo SU worktree (MONO-0). Cruce deliberado: KJ_ALLOW_CROSS_LANE=1, queda registrado.");
|
|
248
287
|
return true;
|
|
249
288
|
};
|
|
@@ -270,7 +309,7 @@ process.stdin.on("end", () => {
|
|
|
270
309
|
// directorio se deniega conservadoramente — el remedio es no
|
|
271
310
|
// necesitarlo (git -C, npm --prefix, rutas absolutas).
|
|
272
311
|
if (/(^|[;&|(\\s])(cd|pushd)([ \\t]|$)/.test(cmd)) {
|
|
273
|
-
if (
|
|
312
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
274
313
|
else {
|
|
275
314
|
console.error("kj sentinel: cd/pushd en un comando mutador — las rutas posteriores no son verificables por el guard de carriles (MONO-0); usa git -C / npm --prefix / rutas ABSOLUTAS sin cd (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
276
315
|
process.exit(2);
|
|
@@ -289,7 +328,7 @@ process.stdin.on("end", () => {
|
|
|
289
328
|
// de tokens de abajo; el residuo (expansion anidada en segmentos
|
|
290
329
|
// runner) queda documentado como en la regla de ficheros PROTECTED.
|
|
291
330
|
if (/>{1,2}[ \\t]*["']?[\\$\`]/.test(cmd)) {
|
|
292
|
-
if (
|
|
331
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
293
332
|
else {
|
|
294
333
|
console.error("kj sentinel: redireccion con destino tras variable/sustitucion — el guard de carriles no puede verificarlo (MONO-0); usa una ruta LITERAL (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
295
334
|
process.exit(2);
|
|
@@ -355,7 +394,7 @@ process.stdin.on("end", () => {
|
|
|
355
394
|
return flush();
|
|
356
395
|
};
|
|
357
396
|
if (/\\$\\(|\`/.test(cmd) || quotedPathWithSpaces(cmd)) {
|
|
358
|
-
if (
|
|
397
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
359
398
|
else {
|
|
360
399
|
console.error("kj sentinel: sustitucion de comandos o ruta entrecomillada con espacios en un comando mutador — no verificable por el guard de carriles (MONO-0); usa valores/rutas LITERALES sin sustitucion (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
361
400
|
process.exit(2);
|
|
@@ -364,7 +403,7 @@ process.stdin.on("end", () => {
|
|
|
364
403
|
const SAFE_EXP_SEG = /^([A-Za-z_][A-Za-z0-9_]*=[^ \\t]*[ \\t]*)*((npm|pnpm|yarn|vitest|jest|kj|gh|echo|printf|true|test)\\b|git[ \\t](?![^\\n]*(-C[ \\t]|--git-dir|--work-tree)))[^;|&\\n]*$|^[A-Za-z_][A-Za-z0-9_]*=[^;|&\\n]*$/;
|
|
365
404
|
const segs = cmd.split(/&&|\\|\\||[;|\\n]/).map((s) => s.trim()).filter(Boolean);
|
|
366
405
|
if (!segs.every((s) => !/[\\$]/.test(s) || SAFE_EXP_SEG.test(s))) {
|
|
367
|
-
if (
|
|
406
|
+
if (escOn("KJ_ALLOW_CROSS_LANE")) { recordEscape(sid, "KJ_ALLOW_CROSS_LANE", tool); }
|
|
368
407
|
else {
|
|
369
408
|
console.error("kj sentinel: expansion de shell en una herramienta generica de fichero/interprete — el objetivo no es verificable por el guard de carriles (MONO-0); usa rutas LITERALES o un runner del toolchain (o KJ_ALLOW_CROSS_LANE=1, queda registrado).");
|
|
370
409
|
process.exit(2);
|
|
@@ -406,13 +445,13 @@ process.stdin.on("end", () => {
|
|
|
406
445
|
let v = null;
|
|
407
446
|
try { v = JSON.parse(String(pres.stdout || "").trim().split("\\n").pop()); } catch { /* mensaje generico */ }
|
|
408
447
|
const secure = !!(v && v.class === "security");
|
|
409
|
-
if (!secure &&
|
|
448
|
+
if (!secure && escOn("KJ_ALLOW_POLICY")) { recordEscape(sid, "KJ_ALLOW_POLICY", tool); }
|
|
410
449
|
else {
|
|
411
450
|
console.error("kj sentinel: policy deny [" + ((v && v.rule_id) || "policy") + "] " + ((v && v.reason) || "la tool call viola la policy del proyecto") + (secure ? " [security — sin escape ni arbitraje]" : " (KJ_ALLOW_POLICY=1 = excepcion consciente, queda registrada; el commit exigira ademas KJ_POLICY_REASON)"));
|
|
412
451
|
process.exit(2);
|
|
413
452
|
}
|
|
414
453
|
} else if (pres.status !== 0) {
|
|
415
|
-
if (
|
|
454
|
+
if (escOn("KJ_ALLOW_POLICY")) { recordEscape(sid, "KJ_ALLOW_POLICY", tool); }
|
|
416
455
|
else {
|
|
417
456
|
console.error("kj sentinel: kj policy eval fallo (exit " + pres.status + ") — la policy declarada no se pudo evaluar, deny por defecto; diagnostica con kj policy check y corrige .karajan/policy.yml fuera de la sesion (o KJ_ALLOW_POLICY=1 = excepcion consciente, queda registrada).");
|
|
418
457
|
process.exit(2);
|
|
@@ -426,7 +465,7 @@ process.stdin.on("end", () => {
|
|
|
426
465
|
const branch = branchOf();
|
|
427
466
|
const why = !branch ? null : BASE_BRANCHES.has(branch) ? "base" : !CARD.test(branch) ? "nocard" : null;
|
|
428
467
|
if (why) {
|
|
429
|
-
if (
|
|
468
|
+
if (escOn("KJ_ALLOW_NO_CARD")) { recordEscape(sid, "KJ_ALLOW_NO_CARD", tool); process.exit(0); }
|
|
430
469
|
console.error(why === "base"
|
|
431
470
|
? "kj sentinel: no se editan fuentes en la rama base '" + branch + "' — crea la card (kj hu add) y la rama: git checkout -b feat/<CARD-ID>-descripcion. (KJ_ALLOW_NO_CARD=1 = excepcion consciente, queda registrada)"
|
|
432
471
|
: "kj sentinel: la rama '" + branch + "' no referencia ninguna card — crea/mueve la card a running (kj hu add | kj hu move) y usa una rama feat/<CARD-ID>-descripcion. (KJ_ALLOW_NO_CARD=1 = excepcion consciente, queda registrada)");
|
|
@@ -437,7 +476,7 @@ process.stdin.on("end", () => {
|
|
|
437
476
|
if (tool === "Bash") {
|
|
438
477
|
const cmd = String(input.command || "");
|
|
439
478
|
if (PUBLISH.test(cmd)) {
|
|
440
|
-
if (
|
|
479
|
+
if (escOn("KJ_ALLOW_RELEASE")) { recordEscape(sid, "KJ_ALLOW_RELEASE", tool); process.exit(0); }
|
|
441
480
|
const res = spawnSync("kj", ["release", "check", "--json"], { cwd: ROOT, encoding: "utf8" });
|
|
442
481
|
if (res.error || res.status === null) process.exit(0);
|
|
443
482
|
if (res.status !== 0) {
|
package/src/roles/audit-role.js
CHANGED
|
@@ -6,6 +6,7 @@ import { collectSonarFindings } from "../audit/sonar-findings.js";
|
|
|
6
6
|
import { collectWebPerfInput } from "../audit/webperf-input.js";
|
|
7
7
|
import { collectOsvFindings } from "../audit/osv-findings.js";
|
|
8
8
|
import { collectSemgrepFindings } from "../audit/semgrep-findings.js";
|
|
9
|
+
import { collectInfraFindings } from "../audit/infra-findings.js";
|
|
9
10
|
import { collectCircularDeps } from "../audit/circular-deps.js";
|
|
10
11
|
import { collectDeadExports } from "../audit/dead-exports.js";
|
|
11
12
|
import { collectInjectionFindings } from "../audit/injection-findings.js";
|
|
@@ -58,6 +59,7 @@ export class AuditRole extends AgentRole {
|
|
|
58
59
|
const noMadge = (typeof input === "object" ? Boolean(input?.noMadge) : false) || securityOnly;
|
|
59
60
|
const noKnip = (typeof input === "object" ? Boolean(input?.noKnip) : false) || securityOnly;
|
|
60
61
|
const noInjectionScan = typeof input === "object" ? Boolean(input?.noInjectionScan) : false;
|
|
62
|
+
const noInfraScan = typeof input === "object" ? Boolean(input?.noInfraScan) : false;
|
|
61
63
|
const noAiSlop = (typeof input === "object" ? Boolean(input?.noAiSlop) : false) || securityOnly;
|
|
62
64
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
63
65
|
let basalCost = null;
|
|
@@ -70,6 +72,7 @@ export class AuditRole extends AgentRole {
|
|
|
70
72
|
let circularDeps = null;
|
|
71
73
|
let deadExports = null;
|
|
72
74
|
let injectionFindings = null;
|
|
75
|
+
let infraFindings = null;
|
|
73
76
|
let aiSlop = null;
|
|
74
77
|
if (!securityOnly) {
|
|
75
78
|
try {
|
|
@@ -127,6 +130,14 @@ export class AuditRole extends AgentRole {
|
|
|
127
130
|
injectionFindings = await collectInjectionFindings(projectDir, this.logger);
|
|
128
131
|
} catch { /* injection scan is best-effort (KJC-TSK-0498) */ }
|
|
129
132
|
}
|
|
133
|
+
// Checkov infra misconfigs — INF-C (KJC-TSK-0760). Security surface, so
|
|
134
|
+
// it ALSO runs in securityOnly. Best-effort: no infra markers or missing
|
|
135
|
+
// checkov return available:false declaring why.
|
|
136
|
+
if (!noInfraScan) {
|
|
137
|
+
try {
|
|
138
|
+
infraFindings = await collectInfraFindings(projectDir, this.logger);
|
|
139
|
+
} catch { /* checkov is best-effort */ }
|
|
140
|
+
}
|
|
130
141
|
// AI-slop tells (KJC-TSK-0503). Deterministic, offline, regex-based.
|
|
131
142
|
// Local equivalent of Deslopify-style review. Best-effort.
|
|
132
143
|
if (!noAiSlop) {
|
|
@@ -134,7 +145,7 @@ export class AuditRole extends AgentRole {
|
|
|
134
145
|
aiSlop = await collectAiSlop(projectDir);
|
|
135
146
|
} catch { /* ai-slop scan is best-effort */ }
|
|
136
147
|
}
|
|
137
|
-
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, injectionFindings, aiSlop };
|
|
148
|
+
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings, circularDeps, deadExports, injectionFindings, infraFindings, aiSlop };
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
/**
|
package/src/roles/tester-role.js
CHANGED
|
@@ -24,7 +24,14 @@ const COVERAGE_COMMANDS = {
|
|
|
24
24
|
rspec: "bundle exec rspec 2>&1",
|
|
25
25
|
phpunit: "vendor/bin/phpunit --coverage-text 2>&1",
|
|
26
26
|
"dotnet-test": "dotnet test --collect:\"XPlat Code Coverage\" 2>&1",
|
|
27
|
-
"dart-test": "dart test 2>&1"
|
|
27
|
+
"dart-test": "dart test 2>&1",
|
|
28
|
+
// INF-B: the basic suite ALWAYS runs first; checkov is the additive deep
|
|
29
|
+
// scan. If checkov is missing this chain fails and the brief falls back to
|
|
30
|
+
// the basic suite alone (declared degradation, never green-by-omission).
|
|
31
|
+
"terraform-validate": "terraform init -backend=false -input=false 2>&1 && terraform validate 2>&1 && checkov -d . --compact --quiet 2>&1",
|
|
32
|
+
"helm-lint": "helm lint . 2>&1 && checkov -d . --compact --quiet 2>&1",
|
|
33
|
+
"kustomize-build": "kustomize build . > /dev/null && checkov -d . --compact --quiet 2>&1",
|
|
34
|
+
"ansible-lint": "ansible-lint 2>&1 && checkov -d . --compact --quiet 2>&1"
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
const TEST_COMMANDS = {
|
|
@@ -39,7 +46,12 @@ const TEST_COMMANDS = {
|
|
|
39
46
|
rspec: "bundle exec rspec 2>&1",
|
|
40
47
|
phpunit: "vendor/bin/phpunit 2>&1",
|
|
41
48
|
"dotnet-test": "dotnet test 2>&1",
|
|
42
|
-
"dart-test": "dart test 2>&1"
|
|
49
|
+
"dart-test": "dart test 2>&1",
|
|
50
|
+
// INF-B (KJC-TSK-0759): infra suites — validity IS the test floor.
|
|
51
|
+
"terraform-validate": "terraform init -backend=false -input=false 2>&1 && terraform validate 2>&1",
|
|
52
|
+
"helm-lint": "helm lint . 2>&1",
|
|
53
|
+
"kustomize-build": "kustomize build . > /dev/null && echo kustomize build: OK",
|
|
54
|
+
"ansible-lint": "ansible-lint 2>&1"
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
export class TesterRole extends AgentRole {
|
|
@@ -82,9 +94,14 @@ export class TesterRole extends AgentRole {
|
|
|
82
94
|
if (detection.hasTests && detection.framework) {
|
|
83
95
|
const coverageCmd = COVERAGE_COMMANDS[detection.framework];
|
|
84
96
|
const testCmd = TEST_COMMANDS[detection.framework];
|
|
97
|
+
// INF-B: infra suites have no node_modules — the step-0 contract is
|
|
98
|
+
// "the tool exists or you SAY which one is missing", never a fake green.
|
|
99
|
+
const step0 = detection.language === "infra"
|
|
100
|
+
? "**Step 0**: Verify the SUITE tool is installed (`which terraform`/`helm`/`kustomize`/`ansible-lint`). If the suite tool is MISSING, do NOT fake a green: report it in `failures` with the exact install command and set tests_pass: false. `checkov` (the deep scan below) is OPTIONAL: if it is missing, run the basic suite as the fallback and SAY the deep scan was skipped (degraded, never green-by-omission)."
|
|
101
|
+
: "**Step 0**: If node_modules/ does not exist, run `npm install` (or `pnpm install`) first.";
|
|
85
102
|
sections.push(
|
|
86
103
|
`## Detected test framework: ${detection.framework} (${detection.language})`,
|
|
87
|
-
|
|
104
|
+
step0,
|
|
88
105
|
`**Step 1**: Run the test suite with coverage:`,
|
|
89
106
|
"```bash",
|
|
90
107
|
coverageCmd || testCmd,
|
package/src/start/sweep.js
CHANGED
|
@@ -112,6 +112,9 @@ function readRagStatus(projectDir) {
|
|
|
112
112
|
* declared = user-declared maturity; deps = injected collaborators (defaults = real collectors).
|
|
113
113
|
*/
|
|
114
114
|
export async function runReadOnlySweep(projectDir, { declared = null, profile = "standard", deps = {} } = {}) {
|
|
115
|
+
// KJC-BUG-0145: a missing projectDir must fail LOUD — every collector is
|
|
116
|
+
// wrapped in safe(), so an undefined root used to read as an empty project.
|
|
117
|
+
if (!projectDir) throw new Error("runReadOnlySweep: projectDir is required (got " + String(projectDir) + ")");
|
|
115
118
|
const d = {
|
|
116
119
|
collectAll, detectTestFramework, checkHarden, compareHarden, sourceScan: deepSourceScan,
|
|
117
120
|
detectQmd, ragStatus: readRagStatus, gitAgeDays: lastCommitAgeDays, ...deps,
|
|
@@ -42,6 +42,18 @@ const LANGUAGE_MARKERS = [
|
|
|
42
42
|
{ file: "pubspec.yaml", framework: "dart-test", language: "dart" },
|
|
43
43
|
];
|
|
44
44
|
|
|
45
|
+
// --- Infra (INF-B, KJC-TSK-0759) ---
|
|
46
|
+
// Checked AFTER app-language markers: a mixed repo keeps its app framework.
|
|
47
|
+
// Only unequivocal markers — a stray .yaml never classifies as infra.
|
|
48
|
+
const INFRA_MARKERS = [
|
|
49
|
+
{ file: "*.tf", glob: true, framework: "terraform-validate", language: "infra" },
|
|
50
|
+
{ file: "terraform", framework: "terraform-validate", language: "infra" },
|
|
51
|
+
{ file: "Chart.yaml", framework: "helm-lint", language: "infra" },
|
|
52
|
+
{ file: "kustomization.yaml", framework: "kustomize-build", language: "infra" },
|
|
53
|
+
{ file: "kustomization.yml", framework: "kustomize-build", language: "infra" },
|
|
54
|
+
{ file: "ansible.cfg", framework: "ansible-lint", language: "infra" },
|
|
55
|
+
];
|
|
56
|
+
|
|
45
57
|
/** Test file patterns per language (used by TDD policy). */
|
|
46
58
|
export const TEST_PATTERNS_BY_LANGUAGE = {
|
|
47
59
|
javascript: ["/tests/", "/__tests__/", ".test.", ".spec."],
|
|
@@ -55,6 +67,7 @@ export const TEST_PATTERNS_BY_LANGUAGE = {
|
|
|
55
67
|
php: ["/tests/", "/test/", "Test.php"],
|
|
56
68
|
swift: ["/Tests/", "Tests.swift"],
|
|
57
69
|
dart: ["/test/", "_test.dart"],
|
|
70
|
+
infra: ["/tests/", "/test/", "_test."],
|
|
58
71
|
};
|
|
59
72
|
|
|
60
73
|
function frameworkFromConfig(filename) {
|
|
@@ -93,7 +106,29 @@ export async function detectTestFramework(cwd = process.cwd()) {
|
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
// 3. Multi-language: check project markers
|
|
96
|
-
|
|
109
|
+
const lang = await matchMarkers(cwd, LANGUAGE_MARKERS);
|
|
110
|
+
if (lang) return lang;
|
|
111
|
+
|
|
112
|
+
// 4. Infra (INF-B): terraform/helm/kustomize/ansible have their own suite.
|
|
113
|
+
const infra = await matchMarkers(cwd, INFRA_MARKERS);
|
|
114
|
+
if (infra) return infra;
|
|
115
|
+
|
|
116
|
+
return { hasTests: false, framework: null, language: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* True when the project carries unequivocal infra markers (terraform, helm,
|
|
121
|
+
* kustomize, ansible) — regardless of the app framework the repo may also
|
|
122
|
+
* have. Used by INF-C's audit collector to gate the checkov scan.
|
|
123
|
+
* @param {string} cwd
|
|
124
|
+
* @returns {Promise<boolean>}
|
|
125
|
+
*/
|
|
126
|
+
export async function detectInfraMarkers(cwd = process.cwd()) {
|
|
127
|
+
return Boolean(await matchMarkers(cwd, INFRA_MARKERS));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function matchMarkers(cwd, markers) {
|
|
131
|
+
for (const marker of markers) {
|
|
97
132
|
try {
|
|
98
133
|
if (marker.glob) {
|
|
99
134
|
const entries = await fs.readdir(cwd);
|
|
@@ -107,8 +142,7 @@ export async function detectTestFramework(cwd = process.cwd()) {
|
|
|
107
142
|
}
|
|
108
143
|
} catch { /* not found */ }
|
|
109
144
|
}
|
|
110
|
-
|
|
111
|
-
return { hasTests: false, framework: null, language: null };
|
|
145
|
+
return null;
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
/**
|