@lchase/kiln 0.8.0 → 0.9.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/README.md +2 -0
- package/dist/cli.js +49 -2
- package/dist/delivery.js +18 -0
- package/dist/driver.js +98 -0
- package/dist/events.js +354 -0
- package/dist/evidence.js +140 -0
- package/dist/jobs.js +12 -0
- package/dist/ledger.js +144 -0
- package/dist/project.js +378 -0
- package/dist/redact.js +63 -0
- package/dist/reliability.js +23 -2
- package/dist/serve.js +16 -1
- package/package.json +1 -1
package/dist/redact.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Masks secrets in text Kiln writes to the event log, the ledger, and a
|
|
3
|
+
* pull request body. One function owns that. A log or a PR is public-shaped
|
|
4
|
+
* even when a test command or a check excerpt is not.
|
|
5
|
+
*
|
|
6
|
+
* Token shapes cover the common issued-credential prefixes. A value is also
|
|
7
|
+
* masked when it belongs to an env assignment, a JSON field, or an auth
|
|
8
|
+
* header whose name contains TOKEN, KEY, SECRET, or PASSWORD.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_NAME = /token|key|secret|password/i;
|
|
11
|
+
const TOKEN_SHAPES = [
|
|
12
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g,
|
|
13
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
14
|
+
/\bglpat-[A-Za-z0-9\-_]{16,}\b/g,
|
|
15
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
16
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
17
|
+
/\bsk-ant-[A-Za-z0-9\-_]{10,}\b/g,
|
|
18
|
+
/\bsk-[A-Za-z0-9]{20,}\b/g,
|
|
19
|
+
/\bnpm_[A-Za-z0-9]{20,}\b/g,
|
|
20
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
21
|
+
];
|
|
22
|
+
const AUTH_HEADER = /\b(authorization|proxy-authorization|x-api-key|x-auth-token)\s*:[^\n]*/gi;
|
|
23
|
+
const BEARER = /\bBearer\s+[A-Za-z0-9\-._~+/]{8,}={0,2}/g;
|
|
24
|
+
const ENV_ASSIGN = /(^|[\n\r;\s])((?:export[ \t]+)?)([A-Za-z_][A-Za-z0-9_]*)([ \t]*=[ \t]*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s#]+)/g;
|
|
25
|
+
const JSON_FIELD = /"([A-Za-z_][A-Za-z0-9_]*)"(\s*:\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g;
|
|
26
|
+
/** Replace secret-shaped text with `[redacted]`. Idempotent on its own output. */
|
|
27
|
+
export function redact(text) {
|
|
28
|
+
let out = text.replace(ENV_ASSIGN, (full, lead, prefix, name, eq, value) => {
|
|
29
|
+
if (!SECRET_NAME.test(name))
|
|
30
|
+
return full;
|
|
31
|
+
return `${lead}${prefix}${name}${eq}[redacted]`;
|
|
32
|
+
});
|
|
33
|
+
out = out.replace(JSON_FIELD, (full, name, mid, value) => {
|
|
34
|
+
if (!SECRET_NAME.test(name))
|
|
35
|
+
return full;
|
|
36
|
+
const quote = value.startsWith("'") ? "'" : '"';
|
|
37
|
+
return `"${name}"${mid}${quote}[redacted]${quote}`;
|
|
38
|
+
});
|
|
39
|
+
out = out.replace(AUTH_HEADER, (_full, name) => `${name}: [redacted]`);
|
|
40
|
+
out = out.replace(BEARER, "Bearer [redacted]");
|
|
41
|
+
for (const pattern of TOKEN_SHAPES) {
|
|
42
|
+
out = out.replace(pattern, "[redacted]");
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** Walk a JSON value and redact every string. Object keys that name a secret drop the value. */
|
|
47
|
+
export function redactDeep(value) {
|
|
48
|
+
if (typeof value === "string")
|
|
49
|
+
return redact(value);
|
|
50
|
+
if (typeof value !== "object" || value === null)
|
|
51
|
+
return value;
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value.map((item) => redactDeep(item));
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
56
|
+
if (typeof inner === "string" && SECRET_NAME.test(key)) {
|
|
57
|
+
out[key] = "[redacted]";
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
out[key] = redactDeep(inner);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
package/dist/reliability.js
CHANGED
|
@@ -4,6 +4,7 @@ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync
|
|
|
4
4
|
import { dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { authenticationFailureLine } from "./auth.js";
|
|
7
|
+
import { appendEvent } from "./events.js";
|
|
7
8
|
import { jobBasename } from "./paths.js";
|
|
8
9
|
import { FAILURE_REASONS } from "./reconcile.js";
|
|
9
10
|
/**
|
|
@@ -105,6 +106,17 @@ export function recoverRuns(config, jobs, options) {
|
|
|
105
106
|
const recorded = recordFailure(config, job.jobId, existing, pending, options.now);
|
|
106
107
|
if (recorded.created) {
|
|
107
108
|
appendLog(job.logPath, recorded.ledger.attempts.at(-1));
|
|
109
|
+
const runId = pending.runId ?? job.run?.runId;
|
|
110
|
+
appendEvent(config.kilnDir, {
|
|
111
|
+
job_id: job.jobId,
|
|
112
|
+
run_id: runId ?? null,
|
|
113
|
+
event: "run.failed",
|
|
114
|
+
properties: {
|
|
115
|
+
reason: pending.reason,
|
|
116
|
+
...(pending.detail ? { detail: pending.detail } : {}),
|
|
117
|
+
once: runId ? `run.failed:${runId}` : `run.failed:launch:${recorded.ledger.attempts.length}`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
108
120
|
notes.push({
|
|
109
121
|
jobId: job.jobId,
|
|
110
122
|
kind: kindFor(pending.reason),
|
|
@@ -115,8 +127,17 @@ export function recoverRuns(config, jobs, options) {
|
|
|
115
127
|
}
|
|
116
128
|
const checkout = job.run?.checkout ?? config.repos[job.repo]?.path;
|
|
117
129
|
const runId = pending.runId ?? job.run?.runId;
|
|
118
|
-
if (checkout && runId)
|
|
119
|
-
cleanupRunWorktree(checkout, runId);
|
|
130
|
+
if (checkout && runId) {
|
|
131
|
+
const outcome = cleanupRunWorktree(checkout, runId);
|
|
132
|
+
if (outcome === "removed" || outcome === "kept") {
|
|
133
|
+
appendEvent(config.kilnDir, {
|
|
134
|
+
job_id: job.jobId,
|
|
135
|
+
run_id: runId,
|
|
136
|
+
event: outcome === "removed" ? "worktree.cleaned" : "worktree.kept",
|
|
137
|
+
properties: { once: `worktree.${outcome}:${runId}` },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
120
141
|
}
|
|
121
142
|
return notes;
|
|
122
143
|
}
|
package/dist/serve.js
CHANGED
|
@@ -7,8 +7,9 @@ import { AUTH_PREFLIGHT_PREFIX } from "./auth.js";
|
|
|
7
7
|
import { loadKilnConfig } from "./config.js";
|
|
8
8
|
import { answerJob, readStatus, retryJob, runTick, startupRecovery } from "./driver.js";
|
|
9
9
|
import { browse } from "./fs-browse.js";
|
|
10
|
-
import {
|
|
10
|
+
import { appendEvent, eventsForJob } from "./events.js";
|
|
11
11
|
import { INVESTIGATION_WORKFLOW } from "./findings.js";
|
|
12
|
+
import { defaultHost } from "./host.js";
|
|
12
13
|
import { intakeJobs } from "./intake-http.js";
|
|
13
14
|
import { packageRoot } from "./paths.js";
|
|
14
15
|
import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
@@ -189,6 +190,15 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
189
190
|
sendJson(res, 200, statusDocument(state), secrets);
|
|
190
191
|
return;
|
|
191
192
|
}
|
|
193
|
+
if (path === "/api/events") {
|
|
194
|
+
const jobId = url.searchParams.get("job_id");
|
|
195
|
+
if (!jobId || !JOB_ID.test(jobId)) {
|
|
196
|
+
sendJson(res, 400, { error: "job_id is required." }, secrets);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
sendJson(res, 200, { job_id: jobId, events: eventsForJob(config.kilnDir, jobId) }, secrets);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
192
202
|
const apiJob = /^\/api\/jobs\/([^/]+)$/.exec(path);
|
|
193
203
|
if (apiJob?.[1]) {
|
|
194
204
|
const jobId = parseJobId(apiJob[1]);
|
|
@@ -503,6 +513,11 @@ function refreshLiveConfig(state) {
|
|
|
503
513
|
state.configError = undefined;
|
|
504
514
|
state.mtimeMs = file.mtimeMs;
|
|
505
515
|
state.size = file.size;
|
|
516
|
+
appendEvent(next.kilnDir, {
|
|
517
|
+
job_id: null,
|
|
518
|
+
event: "config.reloaded",
|
|
519
|
+
properties: {},
|
|
520
|
+
});
|
|
506
521
|
}
|
|
507
522
|
catch (error) {
|
|
508
523
|
const message = error instanceof Error ? error.message : String(error);
|