karajan-code 4.31.0 → 4.31.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
CHANGED
|
@@ -10,6 +10,7 @@ import { isAbsolute, join } from "node:path";
|
|
|
10
10
|
import { runCommand } from "../utils/process.js";
|
|
11
11
|
import { loadPrivacyList, scanPaths } from "../privacy/scan.js";
|
|
12
12
|
import { checkStagedDiff, loadPolicy } from "../policy/engine.js";
|
|
13
|
+
import { liftSealedSupervisorViolations } from "../policy/supervisor-verify.js";
|
|
13
14
|
|
|
14
15
|
// KJC-TSK-0769 — the effect boundary: what ships is re-evaluated against the
|
|
15
16
|
// policy IN FORCE now, not the one each PR was merged under. Artifact rules
|
|
@@ -28,7 +29,13 @@ async function policyRangeCheck(projectDir) {
|
|
|
28
29
|
: ["-C", projectDir, "ls-tree", "-r", "--name-only", "HEAD"]);
|
|
29
30
|
if (listed.exitCode !== 0) throw new Error((listed.stderr || "git failed").trim());
|
|
30
31
|
const files = listed.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
31
|
-
const
|
|
32
|
+
const raw = checkStagedDiff(policy, { role: "coder", files, netLinesAdded: null }).filter((v) => !prScoped.has(v.rule_id));
|
|
33
|
+
// KJC-BUG-0178 (ADR 0009): the supervisor seal trips the very rule that
|
|
34
|
+
// protects the supervisor. `kj review` and `kj policy check` already lift
|
|
35
|
+
// what the sealed provenance backs; without this the release check blocked
|
|
36
|
+
// every version cut after a human `kj harden --commit`, by its own seal.
|
|
37
|
+
const sup = liftSealedSupervisorViolations({ projectDir, violations: raw });
|
|
38
|
+
const violations = sup.violations;
|
|
32
39
|
const hard = violations.filter((v) => v.enforcement === "deny");
|
|
33
40
|
const skipped = prScoped.size > 0 ? `; ${prScoped.size} diff-threshold invariant(s) skipped (PR-scoped)` : "";
|
|
34
41
|
if (hard.length > 0) {
|
|
@@ -112,6 +112,24 @@ export function detectNewHomeEntries(before, projectDir) {
|
|
|
112
112
|
* @param {string|null|undefined} transcript — coder's full output text
|
|
113
113
|
* @returns {string[]} filtered leaks (the subset attributable to coder)
|
|
114
114
|
*/
|
|
115
|
+
/**
|
|
116
|
+
* KJC-BUG-0180 — attribution is not severity. `verifyLeaksAgainstTranscript`
|
|
117
|
+
* stays strict on purpose: with no transcript it returns everything. But
|
|
118
|
+
* "the snapshot changed and there is NO evidence the coder did it" is a
|
|
119
|
+
* different fact from "the coder wrote there", and only the second one
|
|
120
|
+
* should kill the run. The flaky CI failure was exactly this: other test
|
|
121
|
+
* files writing under $HOME during the iteration window, an empty
|
|
122
|
+
* transcript from a mocked coder, and the whole flow failing and retrying.
|
|
123
|
+
*
|
|
124
|
+
* @returns {{attributed: string[], unattributed: string[]}} — `attributed`
|
|
125
|
+
* fails the run; `unattributed` is reported and the work goes on.
|
|
126
|
+
*/
|
|
127
|
+
export function splitLeakEvidence(leaks, transcript) {
|
|
128
|
+
const hasTranscript = typeof transcript === "string" && transcript.length > 0;
|
|
129
|
+
const found = verifyLeaksAgainstTranscript(leaks, transcript);
|
|
130
|
+
return hasTranscript ? { attributed: found, unattributed: [] } : { attributed: [], unattributed: found };
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
export function verifyLeaksAgainstTranscript(leaks, transcript) {
|
|
116
134
|
if (!Array.isArray(leaks) || leaks.length === 0) return [];
|
|
117
135
|
if (!transcript || typeof transcript !== "string" || transcript.length === 0) {
|
|
@@ -20,7 +20,7 @@ import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
|
20
20
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
21
21
|
import { buildStandbyState } from "../../brain/standby-store.js";
|
|
22
22
|
import { applyQuotaSimulation } from "../../utils/quota-simulator.js";
|
|
23
|
-
import { snapshotHomeTopLevel, detectNewHomeEntries, formatLeakMessage,
|
|
23
|
+
import { snapshotHomeTopLevel, detectNewHomeEntries, formatLeakMessage, splitLeakEvidence, detectTranscriptCdLeaks } from "../fs-leak-detector.js";
|
|
24
24
|
|
|
25
25
|
export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null }) {
|
|
26
26
|
logger.setContext({ iteration, stage: "coder" });
|
|
@@ -186,9 +186,14 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
186
186
|
// detected entries against the coder's transcript: only flag those
|
|
187
187
|
// the coder demonstrably referenced.
|
|
188
188
|
const coderTranscript = coderExecResult.result?.output || "";
|
|
189
|
-
|
|
189
|
+
// KJC-BUG-0180: only what the transcript ATTRIBUTES to the coder fails the
|
|
190
|
+
// run. Without a transcript there is no evidence it wrote anything, so the
|
|
191
|
+
// finding is reported and the work goes on — a concurrent host-side write
|
|
192
|
+
// under $HOME used to kill the whole flow.
|
|
193
|
+
const { attributed: layer1Leaks, unattributed } = splitLeakEvidence(candidateLeaks, coderTranscript);
|
|
190
194
|
if (candidateLeaks.length > 0 && layer1Leaks.length === 0) {
|
|
191
|
-
|
|
195
|
+
const why = unattributed.length > 0 ? "no coder transcript to attribute them" : "none referenced in the coder transcript";
|
|
196
|
+
logger.warn(`fs-leak-detector: ${candidateLeaks.length} new $HOME entr(y/ies) detected but ${why} — likely a concurrent host-side write, not flagging (#546).`);
|
|
192
197
|
}
|
|
193
198
|
// Layer 2 (BUG-0032): scan the transcript for `cd <abs-out-of-project> && <write-cmd>`
|
|
194
199
|
// patterns the snapshot-diff misses (e.g. when target dir pre-existed).
|
package/src/privacy/scan.js
CHANGED
|
@@ -59,6 +59,17 @@ const SECRET_HEURISTICS = [
|
|
|
59
59
|
// are blanked before redactPII and COUNTED, so a clean result can say
|
|
60
60
|
// "nothing found" or "found and discarded by context" — never the same thing.
|
|
61
61
|
// The personal denylist runs before this: the user's own datum always blocks.
|
|
62
|
+
// KJC-BUG-0195 — a Google key inside a Firebase web config is public by
|
|
63
|
+
// design: it names the project, and what protects the data are the Firestore
|
|
64
|
+
// rules. It still BLOCKS (the shape alone cannot prove which key it is, and a
|
|
65
|
+
// server key next to it would ride along), but the finding stops being a dead
|
|
66
|
+
// end: it names the declared way out, the allowlist, and what actually
|
|
67
|
+
// protects that key. A gate with no exit teaches people to skip the gate.
|
|
68
|
+
// Markers kept unambiguous on purpose (codex caught projectId being generic
|
|
69
|
+
// enough to appear next to a server key): only Firebase-specific ones count.
|
|
70
|
+
const FIREBASE_WEB_MARKERS = /firebaseConfig|authDomain|firebasestorage|firestore\.googleapis\.com|firebaseapp\.com/;
|
|
71
|
+
const FIREBASE_KEY_HINT = "si es la clave web de Firebase (publica por diseno): declarala en el allow de ~/.karajan/privacy.yml y restringela por dominio en la consola de Google";
|
|
72
|
+
|
|
62
73
|
const CONTEXT_DISCARDS = [
|
|
63
74
|
{ type: "git-sha", re: /\b(?:[0-9a-f]{64}|[0-9a-f]{40})\b/g },
|
|
64
75
|
{ type: "doc-domain-email", re: /\b[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)*(?:example\.(?:com|org|net)|test|invalid|localhost|example)\b/g },
|
|
@@ -71,6 +82,7 @@ const CONTEXT_DISCARDS = [
|
|
|
71
82
|
export function scanText(text, { list = loadPrivacyList(), source = "<text>" } = {}) {
|
|
72
83
|
const findings = [];
|
|
73
84
|
let discarded = 0;
|
|
85
|
+
const firebaseWeb = FIREBASE_WEB_MARKERS.test(String(text));
|
|
74
86
|
String(text).split("\n").forEach((line, i) => {
|
|
75
87
|
let probe = line;
|
|
76
88
|
for (const a of list.allow) probe = probe.split(a).join(" ");
|
|
@@ -85,7 +97,8 @@ export function scanText(text, { list = loadPrivacyList(), source = "<text>" } =
|
|
|
85
97
|
re.lastIndex = 0;
|
|
86
98
|
let m;
|
|
87
99
|
while ((m = re.exec(probe)) !== null) {
|
|
88
|
-
|
|
100
|
+
const hint = type === "google-key" && firebaseWeb ? ` — ${FIREBASE_KEY_HINT}` : "";
|
|
101
|
+
findings.push({ severity: "block", type, source, line: i + 1, masked: `${maskValue(m[0])}${hint}` });
|
|
89
102
|
probe = probe.slice(0, m.index) + " ".repeat(m[0].length) + probe.slice(m.index + m[0].length);
|
|
90
103
|
re.lastIndex = m.index + m[0].length;
|
|
91
104
|
}
|