@swestash/agent-lens 0.7.0 → 0.8.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/dist/agent-lens.js +442 -160
- package/dist/agent-lens.js.map +1 -1
- package/package.json +2 -2
- package/web/assets/{CopyButton--e68pO3S.js → CopyButton-CQA_09tY.js} +1 -1
- package/web/assets/{Dashboard-Ce5pBLCZ.js → Dashboard-CtAZNySe.js} +1 -1
- package/web/assets/{SecurityView-C2cpbxxV.js → SecurityView-Dg0hnxA-.js} +1 -1
- package/web/assets/SessionView-Ddk_CWjP.js +12 -0
- package/web/assets/{SkillView-ByxQC40e.js → SkillView-BfEFS2DU.js} +1 -1
- package/web/assets/{SkillsView-D_CwXBGT.js → SkillsView-CG8HFyyk.js} +1 -1
- package/web/assets/{WorkflowView-Ddmg3evU.js → WorkflowView-B6_1UcYD.js} +1 -1
- package/web/assets/{index-BTQM3Zlg.js → index-B9PbFrlb.js} +2 -2
- package/web/assets/index-Dt08Kre7.css +1 -0
- package/web/assets/{index--T4VdWmP.js → index-ksYuAXX3.js} +1 -1
- package/web/assets/{severity-B-8BLd1n.js → severity-CZIEtWx1.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SessionView-Q6Ec76B9.js +0 -12
- package/web/assets/index-DuZ7WdnZ.css +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as readFileSync8 } from "fs";
|
|
4
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
5
5
|
import { dirname as dirname6, join as join12 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
|
|
@@ -947,6 +947,245 @@ var SECURITY_CATEGORIES = [
|
|
|
947
947
|
}
|
|
948
948
|
];
|
|
949
949
|
|
|
950
|
+
// ../core/dist/secrets.js
|
|
951
|
+
var LEAK_PATTERNS = [
|
|
952
|
+
{ name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
|
|
953
|
+
{ name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
|
|
954
|
+
{ name: "ipv4", re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/ },
|
|
955
|
+
{ name: "url", re: /https?:\/\/[^\s"']+/ },
|
|
956
|
+
{ name: "aws-key", re: /AKIA[0-9A-Z]{16}/ },
|
|
957
|
+
{ name: "bearer/token", re: /\b(?:sk|pk|ghp|gho|xox[abp])[-_][A-Za-z0-9]{8,}/ }
|
|
958
|
+
];
|
|
959
|
+
var SECRET_PATTERNS = [
|
|
960
|
+
{ re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g, label: "PRIVATE-KEY" },
|
|
961
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, label: "AWS-KEY" },
|
|
962
|
+
{ re: /\bASIA[0-9A-Z]{16}\b/g, label: "AWS-KEY" },
|
|
963
|
+
{ re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, label: "GITHUB-TOKEN" },
|
|
964
|
+
{ re: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g, label: "GITHUB-TOKEN" },
|
|
965
|
+
{ re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, label: "SLACK-TOKEN" },
|
|
966
|
+
{ re: /\bxapp-[0-9A-Za-z-]{10,}/g, label: "SLACK-TOKEN" },
|
|
967
|
+
{ re: /\bnpm_[A-Za-z0-9]{30,}/g, label: "NPM-TOKEN" },
|
|
968
|
+
{ re: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: "GOOGLE-API-KEY" },
|
|
969
|
+
{ re: /\b[rsp]k_(?:live|test)_[A-Za-z0-9]{10,}\b/g, label: "STRIPE-KEY" },
|
|
970
|
+
{ re: /\bsk-(?:proj|ant|or|svcacct)-[A-Za-z0-9_-]{20,}/g, label: "API-KEY" },
|
|
971
|
+
{ re: /\bsk-[A-Za-z0-9]{20,}\b/g, label: "API-KEY" },
|
|
972
|
+
{ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g, label: "JWT" }
|
|
973
|
+
];
|
|
974
|
+
function maskUrlCredentials(text) {
|
|
975
|
+
return text.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/\s:@"'`]+:[^/\s@"'`]+@/gi, "$1[REDACTED-AUTH]@").replace(/([?&](?:access_?token|api[_-]?key|apikey|token|secret|password|pwd|client[_-]?secret|auth|signature|sig)=)[^&\s"'`<>]+/gi, "$1[REDACTED]");
|
|
976
|
+
}
|
|
977
|
+
var ENV_ASSIGN = /\b([A-Za-z][A-Za-z0-9_]*_(?:TOKEN|SECRET|KEY|PASSWORD|PASSWD|PWD|PW|CREDENTIALS?)|PASSWORD|PASSWD|SECRET|TOKEN)=(["']?)(?!\$)[^\s"'`&;|<>()]+/gi;
|
|
978
|
+
function maskEnvAssignments(text) {
|
|
979
|
+
return text.replace(ENV_ASSIGN, (_m, name, q) => `${name}=${q}[REDACTED]`);
|
|
980
|
+
}
|
|
981
|
+
var PII_PATTERNS = [
|
|
982
|
+
{ re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, label: "EMAIL" },
|
|
983
|
+
{ re: /\b\d{1,3}(?:\.\d{1,3}){3}\b/g, label: "IP" }
|
|
984
|
+
];
|
|
985
|
+
var HOME_USER = /\/(home|Users)\/[^/\s"'`]+/g;
|
|
986
|
+
var ENCODED_HOME_USER = /(?<![A-Za-z0-9])(-(?:home|Users)-)[A-Za-z0-9._]+(?=-)/g;
|
|
987
|
+
var WINDOWS_USER = /([A-Za-z]:\\Users\\)[^\\/\s"'`]+/gi;
|
|
988
|
+
function maskSecrets(text) {
|
|
989
|
+
let out = maskUrlCredentials(text);
|
|
990
|
+
out = maskEnvAssignments(out);
|
|
991
|
+
for (const { re, label } of SECRET_PATTERNS)
|
|
992
|
+
out = out.replace(re, `[${label}]`);
|
|
993
|
+
for (const { re, label } of PII_PATTERNS)
|
|
994
|
+
out = out.replace(re, `[${label}]`);
|
|
995
|
+
out = out.replace(HOME_USER, (_m, root) => `/${root}/user`);
|
|
996
|
+
out = out.replace(ENCODED_HOME_USER, (_m, root) => `${root}user`);
|
|
997
|
+
out = out.replace(WINDOWS_USER, (_m, root) => `${root}user`);
|
|
998
|
+
return out;
|
|
999
|
+
}
|
|
1000
|
+
var SHARE_LEAK = [
|
|
1001
|
+
...SECRET_PATTERNS.map(({ re, label }) => ({ name: label, re: new RegExp(re.source) })),
|
|
1002
|
+
{ name: "email", re: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/ },
|
|
1003
|
+
{ name: "home-path-with-user", re: /\/(?:home|Users)\/(?!user\b)[A-Za-z0-9._-]+/ },
|
|
1004
|
+
{ name: "encoded-home-with-user", re: /(?<![A-Za-z0-9])-(?:home|Users)-(?!user-)[A-Za-z0-9._]+-/ },
|
|
1005
|
+
{ name: "url-credentials", re: /:\/\/[^/\s:@"'`]+:[^/\s@"'`]+@/ }
|
|
1006
|
+
];
|
|
1007
|
+
var USER_STOPLIST = /* @__PURE__ */ new Set([
|
|
1008
|
+
"user",
|
|
1009
|
+
"users",
|
|
1010
|
+
"home",
|
|
1011
|
+
"root",
|
|
1012
|
+
"admin",
|
|
1013
|
+
"ubuntu",
|
|
1014
|
+
"runner",
|
|
1015
|
+
"node",
|
|
1016
|
+
"app",
|
|
1017
|
+
"apps",
|
|
1018
|
+
"data",
|
|
1019
|
+
"tmp",
|
|
1020
|
+
"shared",
|
|
1021
|
+
"public",
|
|
1022
|
+
"guest",
|
|
1023
|
+
"vagrant",
|
|
1024
|
+
"ec2-user",
|
|
1025
|
+
"docker"
|
|
1026
|
+
]);
|
|
1027
|
+
var HOME_OWNER = /(?:\/(?:home|Users)\/|-(?:home|Users)-|[A-Za-z]:\\Users\\)([A-Za-z0-9._]{3,})/g;
|
|
1028
|
+
function deriveHomeUsers(texts) {
|
|
1029
|
+
const found = /* @__PURE__ */ new Set();
|
|
1030
|
+
for (const t of texts) {
|
|
1031
|
+
if (!t)
|
|
1032
|
+
continue;
|
|
1033
|
+
const re = new RegExp(HOME_OWNER.source, "g");
|
|
1034
|
+
let m;
|
|
1035
|
+
while (m = re.exec(t)) {
|
|
1036
|
+
const u = m[1];
|
|
1037
|
+
if (u.length >= 3 && !USER_STOPLIST.has(u.toLowerCase()))
|
|
1038
|
+
found.add(u);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return [...found];
|
|
1042
|
+
}
|
|
1043
|
+
function scrubUsernames(text, users) {
|
|
1044
|
+
let out = text;
|
|
1045
|
+
for (const u of users) {
|
|
1046
|
+
const esc = u.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
1047
|
+
out = out.replace(new RegExp(`(?<![A-Za-z0-9])${esc}(?![A-Za-z0-9])`, "g"), "[USER]");
|
|
1048
|
+
}
|
|
1049
|
+
return out;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ../core/dist/redact-export.js
|
|
1053
|
+
import { createHash } from "crypto";
|
|
1054
|
+
|
|
1055
|
+
// ../core/dist/markdown.js
|
|
1056
|
+
function fence(s) {
|
|
1057
|
+
return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
|
|
1058
|
+
}
|
|
1059
|
+
function sessionToMarkdown(s, events) {
|
|
1060
|
+
const out = [];
|
|
1061
|
+
out.push(`# ${s.title || "Session " + s.id.slice(0, 8)}`);
|
|
1062
|
+
out.push("");
|
|
1063
|
+
const meta = [
|
|
1064
|
+
["Session", s.id],
|
|
1065
|
+
["Source", s.source],
|
|
1066
|
+
["Project", s.project],
|
|
1067
|
+
["Model", s.model],
|
|
1068
|
+
["Started", s.started_at],
|
|
1069
|
+
["Ended", s.ended_at]
|
|
1070
|
+
].filter(([, v]) => v);
|
|
1071
|
+
for (const [k, v] of meta)
|
|
1072
|
+
out.push(`- **${k}:** ${v}`);
|
|
1073
|
+
out.push("");
|
|
1074
|
+
out.push("---");
|
|
1075
|
+
out.push("");
|
|
1076
|
+
for (const e of events) {
|
|
1077
|
+
const who = e.role || e.type;
|
|
1078
|
+
const ts = e.timestamp ? ` _(${e.timestamp})_` : "";
|
|
1079
|
+
if (e.thinking) {
|
|
1080
|
+
out.push(`### \u{1F9E0} thinking${ts}`);
|
|
1081
|
+
out.push("");
|
|
1082
|
+
out.push(fence(e.thinking));
|
|
1083
|
+
out.push("");
|
|
1084
|
+
}
|
|
1085
|
+
if (e.text) {
|
|
1086
|
+
const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
|
|
1087
|
+
out.push(`### ${icon} ${who}${ts}`);
|
|
1088
|
+
out.push("");
|
|
1089
|
+
out.push(fence(e.text));
|
|
1090
|
+
out.push("");
|
|
1091
|
+
}
|
|
1092
|
+
for (const tc of e.toolCalls) {
|
|
1093
|
+
const label = tc.skill_name ? `Skill: ${tc.skill_name}` : tc.agent_type ? `${tc.tool_name} \u2192 ${tc.agent_type}` : tc.tool_name;
|
|
1094
|
+
const status = tc.status ? ` _(${tc.status})_` : "";
|
|
1095
|
+
out.push(`> \u{1F527} **${label}**${status}`);
|
|
1096
|
+
if (tc.input_json && tc.input_json !== "{}") {
|
|
1097
|
+
out.push("```json");
|
|
1098
|
+
out.push(fence(tc.input_json));
|
|
1099
|
+
out.push("```");
|
|
1100
|
+
}
|
|
1101
|
+
out.push("");
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return out.join("\n");
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// ../core/dist/redact-export.js
|
|
1108
|
+
function hash(s) {
|
|
1109
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 4);
|
|
1110
|
+
}
|
|
1111
|
+
function structurePath(p) {
|
|
1112
|
+
if (!p)
|
|
1113
|
+
return p;
|
|
1114
|
+
const abs = p.startsWith("/");
|
|
1115
|
+
const segs = p.split("/").filter(Boolean);
|
|
1116
|
+
const out = segs.map((seg, i) => {
|
|
1117
|
+
if (segs[i - 1] === "home" || segs[i - 1] === "Users")
|
|
1118
|
+
return "user";
|
|
1119
|
+
if (seg === "home" || seg === "Users")
|
|
1120
|
+
return seg;
|
|
1121
|
+
const last = i === segs.length - 1;
|
|
1122
|
+
const dot = last ? seg.lastIndexOf(".") : -1;
|
|
1123
|
+
return dot > 0 ? `file-${hash(seg)}${seg.slice(dot)}` : `dir-${hash(seg)}`;
|
|
1124
|
+
});
|
|
1125
|
+
return (abs ? "/" : "") + out.join("/");
|
|
1126
|
+
}
|
|
1127
|
+
function scrub(s) {
|
|
1128
|
+
return s && s.length > 0 ? "[redacted]" : s;
|
|
1129
|
+
}
|
|
1130
|
+
function redactSession(session, events, level) {
|
|
1131
|
+
const users = deriveHomeUsers([
|
|
1132
|
+
session.title,
|
|
1133
|
+
session.project,
|
|
1134
|
+
session.source,
|
|
1135
|
+
...events.flatMap((e) => [e.text, e.thinking, ...e.toolCalls.map((t) => t.input_json)])
|
|
1136
|
+
]);
|
|
1137
|
+
const unuser = (s) => s == null ? s : scrubUsernames(s, users);
|
|
1138
|
+
if (level === "structure") {
|
|
1139
|
+
return {
|
|
1140
|
+
session: { ...session, title: session.title ? "Redacted session" : session.title, project: structurePath(session.project), source: unuser(session.source) },
|
|
1141
|
+
events: events.map((e) => ({
|
|
1142
|
+
...e,
|
|
1143
|
+
text: scrub(e.text),
|
|
1144
|
+
thinking: scrub(e.thinking),
|
|
1145
|
+
toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: t.input_json ? "{}" : t.input_json }))
|
|
1146
|
+
}))
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
const mask = (s) => s == null ? s : scrubUsernames(maskSecrets(s), users);
|
|
1150
|
+
return {
|
|
1151
|
+
session: { ...session, title: mask(session.title), project: mask(session.project), source: mask(session.source) },
|
|
1152
|
+
events: events.map((e) => ({
|
|
1153
|
+
...e,
|
|
1154
|
+
text: mask(e.text),
|
|
1155
|
+
thinking: mask(e.thinking),
|
|
1156
|
+
toolCalls: e.toolCalls.map((t) => ({ ...t, input_json: mask(t.input_json) }))
|
|
1157
|
+
}))
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function sweep(md, patterns) {
|
|
1161
|
+
let out = md;
|
|
1162
|
+
let caught = false;
|
|
1163
|
+
for (const { re } of patterns) {
|
|
1164
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
|
1165
|
+
if (g.test(out)) {
|
|
1166
|
+
caught = true;
|
|
1167
|
+
out = out.replace(g, "[REDACTED]");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return { md: out, caught };
|
|
1171
|
+
}
|
|
1172
|
+
function disclaimer(level) {
|
|
1173
|
+
return `> \u{1F512} **Redacted export** (level: ${level}) \u2014 best-effort automatic masking of secrets, tokens, emails, IPs, and usernames. Not a guarantee; review before sharing. Generated locally by Agent Lens \u2014 never uploaded.`;
|
|
1174
|
+
}
|
|
1175
|
+
function exportMarkdown(session, events, opts = {}) {
|
|
1176
|
+
const level = opts.level ?? "secrets";
|
|
1177
|
+
if (level === "off") {
|
|
1178
|
+
return { markdown: sessionToMarkdown(session, events), residualLeak: false, level: "off" };
|
|
1179
|
+
}
|
|
1180
|
+
const redacted = redactSession(session, events, level);
|
|
1181
|
+
const rendered = sessionToMarkdown(redacted.session, redacted.events);
|
|
1182
|
+
const scanSet = level === "structure" ? LEAK_PATTERNS : SHARE_LEAK;
|
|
1183
|
+
const { md, caught } = sweep(rendered, scanSet);
|
|
1184
|
+
return { markdown: `${disclaimer(level)}
|
|
1185
|
+
|
|
1186
|
+
${md}`, residualLeak: caught, level };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
950
1189
|
// ../core/dist/errors.js
|
|
951
1190
|
var ERROR_CLASSIFIER_VERSION = 1;
|
|
952
1191
|
var REJECTION_TYPES = /* @__PURE__ */ new Set(["user-rejected", "guardrail-blocked"]);
|
|
@@ -1010,58 +1249,6 @@ function costForUsage(model, u) {
|
|
|
1010
1249
|
return (u.input_tokens * r.input + u.output_tokens * r.output + u.cache_creation_input_tokens * r.cacheWrite + u.cache_read_input_tokens * r.cacheRead) / M;
|
|
1011
1250
|
}
|
|
1012
1251
|
|
|
1013
|
-
// ../core/dist/markdown.js
|
|
1014
|
-
function fence(s) {
|
|
1015
|
-
return s.length > 2e3 ? s.slice(0, 2e3) + "\n\u2026 (truncated)" : s;
|
|
1016
|
-
}
|
|
1017
|
-
function sessionToMarkdown(s, events) {
|
|
1018
|
-
const out = [];
|
|
1019
|
-
out.push(`# ${s.title || "Session " + s.id.slice(0, 8)}`);
|
|
1020
|
-
out.push("");
|
|
1021
|
-
const meta = [
|
|
1022
|
-
["Session", s.id],
|
|
1023
|
-
["Source", s.source],
|
|
1024
|
-
["Project", s.project],
|
|
1025
|
-
["Model", s.model],
|
|
1026
|
-
["Started", s.started_at],
|
|
1027
|
-
["Ended", s.ended_at]
|
|
1028
|
-
].filter(([, v]) => v);
|
|
1029
|
-
for (const [k, v] of meta)
|
|
1030
|
-
out.push(`- **${k}:** ${v}`);
|
|
1031
|
-
out.push("");
|
|
1032
|
-
out.push("---");
|
|
1033
|
-
out.push("");
|
|
1034
|
-
for (const e of events) {
|
|
1035
|
-
const who = e.role || e.type;
|
|
1036
|
-
const ts = e.timestamp ? ` _(${e.timestamp})_` : "";
|
|
1037
|
-
if (e.thinking) {
|
|
1038
|
-
out.push(`### \u{1F9E0} thinking${ts}`);
|
|
1039
|
-
out.push("");
|
|
1040
|
-
out.push(fence(e.thinking));
|
|
1041
|
-
out.push("");
|
|
1042
|
-
}
|
|
1043
|
-
if (e.text) {
|
|
1044
|
-
const icon = who === "user" ? "\u{1F464}" : who === "assistant" ? "\u{1F916}" : "\u2699\uFE0F";
|
|
1045
|
-
out.push(`### ${icon} ${who}${ts}`);
|
|
1046
|
-
out.push("");
|
|
1047
|
-
out.push(fence(e.text));
|
|
1048
|
-
out.push("");
|
|
1049
|
-
}
|
|
1050
|
-
for (const tc of e.toolCalls) {
|
|
1051
|
-
const label = tc.skill_name ? `Skill: ${tc.skill_name}` : tc.agent_type ? `${tc.tool_name} \u2192 ${tc.agent_type}` : tc.tool_name;
|
|
1052
|
-
const status = tc.status ? ` _(${tc.status})_` : "";
|
|
1053
|
-
out.push(`> \u{1F527} **${label}**${status}`);
|
|
1054
|
-
if (tc.input_json && tc.input_json !== "{}") {
|
|
1055
|
-
out.push("```json");
|
|
1056
|
-
out.push(fence(tc.input_json));
|
|
1057
|
-
out.push("```");
|
|
1058
|
-
}
|
|
1059
|
-
out.push("");
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
return out.join("\n");
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
1252
|
// ../core/dist/rawjson.js
|
|
1066
1253
|
import { gzipSync, gunzipSync } from "zlib";
|
|
1067
1254
|
function packRaw(json) {
|
|
@@ -1550,8 +1737,11 @@ After=network.target
|
|
|
1550
1737
|
Type=simple
|
|
1551
1738
|
# Absolute node + CLI paths: systemd user services don't inherit the interactive PATH.
|
|
1552
1739
|
ExecStart=${node} ${cli2} serve
|
|
1553
|
-
${envLines}# Long-running:
|
|
1554
|
-
Restart=on-failure
|
|
1740
|
+
${envLines}# Long-running: always come back, including after a bare SIGTERM. systemd counts SIGTERM as a
|
|
1741
|
+
# clean exit, so Restart=on-failure would leave the server dead after a stray "pkill -f ... serve"
|
|
1742
|
+
# aimed at an ad-hoc test server \u2014 the unit's ExecStart matches that pattern too. Matches the
|
|
1743
|
+
# launchd KeepAlive=true behaviour. An explicit "systemctl --user stop" still stops it for good.
|
|
1744
|
+
Restart=always
|
|
1555
1745
|
RestartSec=5
|
|
1556
1746
|
Nice=10
|
|
1557
1747
|
|
|
@@ -1913,13 +2103,13 @@ function asString(v) {
|
|
|
1913
2103
|
function asInt(v) {
|
|
1914
2104
|
return typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : 0;
|
|
1915
2105
|
}
|
|
1916
|
-
function parentSessionFromPath(
|
|
1917
|
-
const parts =
|
|
2106
|
+
function parentSessionFromPath(path2) {
|
|
2107
|
+
const parts = path2.split("/");
|
|
1918
2108
|
const i = parts.indexOf("subagents");
|
|
1919
2109
|
return i > 0 ? parts[i - 1] : null;
|
|
1920
2110
|
}
|
|
1921
|
-
function workflowRunFromPath(
|
|
1922
|
-
const parts =
|
|
2111
|
+
function workflowRunFromPath(path2) {
|
|
2112
|
+
const parts = path2.split("/");
|
|
1923
2113
|
const i = parts.indexOf("workflows");
|
|
1924
2114
|
return i >= 0 && i + 1 < parts.length ? parts[i + 1] : null;
|
|
1925
2115
|
}
|
|
@@ -1947,18 +2137,18 @@ var ClaudeCodeAdapter = class {
|
|
|
1947
2137
|
const addFrom = (projectsDir, isVersion) => {
|
|
1948
2138
|
const found = [];
|
|
1949
2139
|
recurseJsonl(projectsDir, found);
|
|
1950
|
-
for (const
|
|
1951
|
-
const parts =
|
|
1952
|
-
const sessionId = basename2(
|
|
2140
|
+
for (const path2 of found) {
|
|
2141
|
+
const parts = path2.split("/");
|
|
2142
|
+
const sessionId = basename2(path2, ".jsonl");
|
|
1953
2143
|
const encodedDir = parts[parts.length - 2] ?? "";
|
|
1954
2144
|
files.push({
|
|
1955
|
-
path,
|
|
2145
|
+
path: path2,
|
|
1956
2146
|
sessionId,
|
|
1957
2147
|
encodedDir,
|
|
1958
2148
|
isVersion,
|
|
1959
2149
|
sourceId,
|
|
1960
|
-
parentSessionId: parentSessionFromPath(
|
|
1961
|
-
workflowRunId: workflowRunFromPath(
|
|
2150
|
+
parentSessionId: parentSessionFromPath(path2),
|
|
2151
|
+
workflowRunId: workflowRunFromPath(path2)
|
|
1962
2152
|
});
|
|
1963
2153
|
}
|
|
1964
2154
|
};
|
|
@@ -2354,14 +2544,15 @@ function classify(db, dirty) {
|
|
|
2354
2544
|
}
|
|
2355
2545
|
|
|
2356
2546
|
// ../ingest/dist/detect.js
|
|
2357
|
-
import { createHash } from "crypto";
|
|
2358
|
-
|
|
2547
|
+
import { createHash as createHash2 } from "crypto";
|
|
2548
|
+
import { posix as path } from "path";
|
|
2549
|
+
var DETECTOR_VERSION = 8;
|
|
2359
2550
|
function evidence(s, max = 200) {
|
|
2360
2551
|
const one = s.replace(/\s+/g, " ").trim();
|
|
2361
2552
|
return one.length > max ? one.slice(0, max - 1) + "\u2026" : one;
|
|
2362
2553
|
}
|
|
2363
2554
|
function findingId(toolCallId, ruleId) {
|
|
2364
|
-
return
|
|
2555
|
+
return createHash2("sha1").update(toolCallId).update("\0").update(ruleId).digest("hex").slice(0, 16);
|
|
2365
2556
|
}
|
|
2366
2557
|
var SYSTEM_PATH = /(^|\s)(\/etc\/|\/usr\/|\/bin\/|\/sbin\/|\/boot\/|\/var\/|\/root\/|~\/\.ssh\/|~\/\.aws\/)/;
|
|
2367
2558
|
var HOME_OR_ROOT_TARGET = /(\s|^)(~\/?|\/|\/\*|\$HOME\/?|\*)(\s|$)/;
|
|
@@ -2395,8 +2586,8 @@ function execGeneratedScript(cmd) {
|
|
|
2395
2586
|
const e = t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2396
2587
|
const interp = new RegExp(`\\b(?:sh|bash|zsh|ksh|dash|source)\\s+["']?${e}\\b|(?:^|[\\n;|&(])\\s*\\.\\s+["']?${e}\\b`);
|
|
2397
2588
|
const hasPath = /^(\.\/|\/|~\/)/.test(t);
|
|
2398
|
-
const
|
|
2399
|
-
if (interp.test(cmd) ||
|
|
2589
|
+
const path2 = hasPath ? new RegExp(`(?:^|[\\n;|&(])\\s*${e}(?:\\s|$|[;&|<>])`) : new RegExp(`(?:^|[\\n;|&(])\\s*\\.\\/${e}\\b`);
|
|
2590
|
+
if (interp.test(cmd) || path2.test(cmd))
|
|
2400
2591
|
return t;
|
|
2401
2592
|
}
|
|
2402
2593
|
return null;
|
|
@@ -2410,14 +2601,21 @@ function isAgentOwnedPath(p, configDirs) {
|
|
|
2410
2601
|
const tmp = process.env.TMPDIR;
|
|
2411
2602
|
return !!tmp && p.startsWith(tmp.replace(/\/$/, "") + "/");
|
|
2412
2603
|
}
|
|
2413
|
-
function
|
|
2414
|
-
if (!filePath
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
2604
|
+
function outsideProjectPath(filePath, projectPath, configDirs) {
|
|
2605
|
+
if (!filePath)
|
|
2606
|
+
return null;
|
|
2607
|
+
let abs = filePath;
|
|
2608
|
+
if (!filePath.startsWith("/")) {
|
|
2609
|
+
if (!projectPath || !filePath.includes("../"))
|
|
2610
|
+
return null;
|
|
2611
|
+
abs = path.normalize(projectPath.replace(/\/$/, "") + "/" + filePath);
|
|
2612
|
+
}
|
|
2613
|
+
if (isAgentOwnedPath(abs, configDirs))
|
|
2614
|
+
return null;
|
|
2418
2615
|
if (!projectPath)
|
|
2419
|
-
return
|
|
2420
|
-
|
|
2616
|
+
return null;
|
|
2617
|
+
const root = projectPath.replace(/\/$/, "");
|
|
2618
|
+
return abs !== root && !abs.startsWith(root + "/") ? abs : null;
|
|
2421
2619
|
}
|
|
2422
2620
|
var SCOPE_RANK = { unknown: 0, loopback: 1, internal: 2, external: 3 };
|
|
2423
2621
|
function netTargetScope(rawCommand) {
|
|
@@ -2697,6 +2895,34 @@ var RULES = [
|
|
|
2697
2895
|
return { evidence: evidence(ctx.command) };
|
|
2698
2896
|
}
|
|
2699
2897
|
},
|
|
2898
|
+
{
|
|
2899
|
+
id: "obfuscated.decode_exec",
|
|
2900
|
+
category: "privilege-bypass",
|
|
2901
|
+
framework_ref: "OWASP LLM06",
|
|
2902
|
+
title: "Decodes and executes obfuscated content (base64 -d | sh)",
|
|
2903
|
+
tools: ["Bash"],
|
|
2904
|
+
baseSeverity: "high",
|
|
2905
|
+
test(ctx) {
|
|
2906
|
+
const c = ctx.commandBare;
|
|
2907
|
+
if (!c)
|
|
2908
|
+
return null;
|
|
2909
|
+
const decoder = /\b(base64\s+(?:-d|-D|--decode)|xxd\s+-r|openssl\s+(?:base64\s+-d|enc\b[^|]*\s-d\b)|uudecode)\b/i.exec(c);
|
|
2910
|
+
if (!decoder)
|
|
2911
|
+
return null;
|
|
2912
|
+
const downstream = c.slice(decoder.index + decoder[0].length);
|
|
2913
|
+
const piped = /\|\s*(?:sudo\s+)?(sh|bash|zsh|dash|ksh|python\d?|node|perl|ruby)\b([^|;&\n]*)/i.exec(downstream);
|
|
2914
|
+
if (!piped)
|
|
2915
|
+
return null;
|
|
2916
|
+
const interp = piped[1].toLowerCase();
|
|
2917
|
+
const args = piped[2] || "";
|
|
2918
|
+
if (/^(?:node|python|perl|ruby)/.test(interp) && /(?:^|\s)-(?:e|c|m|p)\b|(?:^|\s)--(?:eval|command|module|print)\b/.test(args))
|
|
2919
|
+
return null;
|
|
2920
|
+
const sudo = /\|\s*sudo\b/i.test(downstream);
|
|
2921
|
+
const d = decoder[0].toLowerCase();
|
|
2922
|
+
const decoderName = d.startsWith("base64") ? "base64" : d.startsWith("xxd") ? "xxd" : d.startsWith("openssl") ? "openssl" : "uudecode";
|
|
2923
|
+
return { evidence: evidence(ctx.command), severity: sudo ? "critical" : "high", mods: { decoder: decoderName, interpreter: interp } };
|
|
2924
|
+
}
|
|
2925
|
+
},
|
|
2700
2926
|
{
|
|
2701
2927
|
id: "privilege.sudo",
|
|
2702
2928
|
category: "privilege-bypass",
|
|
@@ -2770,10 +2996,16 @@ var RULES = [
|
|
|
2770
2996
|
// writing to /tmp, scratch dirs, adjacent repos is routine; only a system path (below) is high
|
|
2771
2997
|
test(ctx) {
|
|
2772
2998
|
const f = ctx.filePath;
|
|
2773
|
-
|
|
2999
|
+
const abs = outsideProjectPath(f, ctx.projectPath, ctx.ownedConfigDirs);
|
|
3000
|
+
if (!abs)
|
|
2774
3001
|
return null;
|
|
2775
|
-
const system =
|
|
2776
|
-
|
|
3002
|
+
const system = SYSTEM_PATH.test(abs);
|
|
3003
|
+
const traversal = !!f && !f.startsWith("/") && f.includes("../");
|
|
3004
|
+
return {
|
|
3005
|
+
evidence: evidence(f),
|
|
3006
|
+
severity: system ? "high" : "low",
|
|
3007
|
+
mods: { project: ctx.projectPath, system_path: system, ...traversal ? { traversal: true } : {} }
|
|
3008
|
+
};
|
|
2777
3009
|
}
|
|
2778
3010
|
}
|
|
2779
3011
|
];
|
|
@@ -2896,7 +3128,7 @@ function classifyErrors(db, dirty) {
|
|
|
2896
3128
|
}
|
|
2897
3129
|
|
|
2898
3130
|
// ../ingest/dist/pipeline.js
|
|
2899
|
-
import { createHash as
|
|
3131
|
+
import { createHash as createHash3 } from "crypto";
|
|
2900
3132
|
function newStats() {
|
|
2901
3133
|
return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
|
|
2902
3134
|
}
|
|
@@ -2934,7 +3166,7 @@ function skillSummary(body) {
|
|
|
2934
3166
|
return null;
|
|
2935
3167
|
}
|
|
2936
3168
|
function skillVersionId(name, body) {
|
|
2937
|
-
return
|
|
3169
|
+
return createHash3("sha1").update(name).update("\0").update(body).digest("hex").slice(0, 16);
|
|
2938
3170
|
}
|
|
2939
3171
|
function pruneExcluded(db, excludedPaths) {
|
|
2940
3172
|
if (!excludedPaths.length)
|
|
@@ -3316,7 +3548,7 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
3316
3548
|
}
|
|
3317
3549
|
let projectId = null;
|
|
3318
3550
|
if (sessionMeta.cwd) {
|
|
3319
|
-
projectId =
|
|
3551
|
+
projectId = createHash3("sha1").update(`${adapter.agentId}\0${sessionMeta.cwd}`).digest("hex").slice(0, 16);
|
|
3320
3552
|
stmts.upsertProject.run({
|
|
3321
3553
|
id: projectId,
|
|
3322
3554
|
agent_id: adapter.agentId,
|
|
@@ -3355,7 +3587,7 @@ import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as
|
|
|
3355
3587
|
import { basename as basename3, join as join5 } from "path";
|
|
3356
3588
|
|
|
3357
3589
|
// ../ingest/dist/redact.js
|
|
3358
|
-
import { createHash as
|
|
3590
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
3359
3591
|
function parseExcludes(csv) {
|
|
3360
3592
|
return (csv ?? "").split(",").map((s) => s.trim()).filter(Boolean).map(encodeProjectPath);
|
|
3361
3593
|
}
|
|
@@ -3364,17 +3596,17 @@ function isExcludedArchivePath(filePath, excluded) {
|
|
|
3364
3596
|
}
|
|
3365
3597
|
|
|
3366
3598
|
// ../ingest/dist/fileread.js
|
|
3367
|
-
import { createHash as
|
|
3599
|
+
import { createHash as createHash5 } from "crypto";
|
|
3368
3600
|
import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
|
|
3369
3601
|
import { StringDecoder } from "string_decoder";
|
|
3370
3602
|
var STREAM_THRESHOLD = 8 * 1024 * 1024;
|
|
3371
3603
|
var CHUNK2 = 64 * 1024;
|
|
3372
3604
|
function sha256(buf) {
|
|
3373
|
-
return
|
|
3605
|
+
return createHash5("sha256").update(buf).digest("hex");
|
|
3374
3606
|
}
|
|
3375
|
-
function sha256File(
|
|
3376
|
-
const fd = openSync2(
|
|
3377
|
-
const h =
|
|
3607
|
+
function sha256File(path2) {
|
|
3608
|
+
const fd = openSync2(path2, "r");
|
|
3609
|
+
const h = createHash5("sha256");
|
|
3378
3610
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3379
3611
|
try {
|
|
3380
3612
|
let n;
|
|
@@ -3385,8 +3617,8 @@ function sha256File(path) {
|
|
|
3385
3617
|
}
|
|
3386
3618
|
return h.digest("hex");
|
|
3387
3619
|
}
|
|
3388
|
-
function* streamLines(
|
|
3389
|
-
const fd = openSync2(
|
|
3620
|
+
function* streamLines(path2) {
|
|
3621
|
+
const fd = openSync2(path2, "r");
|
|
3390
3622
|
const decoder = new StringDecoder("utf8");
|
|
3391
3623
|
const buf = Buffer.allocUnsafe(CHUNK2);
|
|
3392
3624
|
let leftover = "";
|
|
@@ -3438,8 +3670,8 @@ function discoverSidecars(sourceArchiveDir) {
|
|
|
3438
3670
|
}
|
|
3439
3671
|
return out;
|
|
3440
3672
|
}
|
|
3441
|
-
function sessionFromPath(
|
|
3442
|
-
const parts =
|
|
3673
|
+
function sessionFromPath(path2) {
|
|
3674
|
+
const parts = path2.split("/");
|
|
3443
3675
|
const i = parts.indexOf("workflows");
|
|
3444
3676
|
return i > 0 ? parts[i - 1] : null;
|
|
3445
3677
|
}
|
|
@@ -3486,25 +3718,25 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3486
3718
|
total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
|
|
3487
3719
|
duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
3488
3720
|
ingested_at=excluded.ingested_at`);
|
|
3489
|
-
for (const
|
|
3721
|
+
for (const path2 of paths) {
|
|
3490
3722
|
stats.files++;
|
|
3491
|
-
const st = statSync2(
|
|
3723
|
+
const st = statSync2(path2);
|
|
3492
3724
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3493
|
-
const prev = full ? void 0 : getState.get(
|
|
3725
|
+
const prev = full ? void 0 : getState.get(path2);
|
|
3494
3726
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3495
3727
|
stats.skipped++;
|
|
3496
3728
|
continue;
|
|
3497
3729
|
}
|
|
3498
3730
|
let buf;
|
|
3499
3731
|
try {
|
|
3500
|
-
buf = readFileSync4(
|
|
3732
|
+
buf = readFileSync4(path2);
|
|
3501
3733
|
} catch {
|
|
3502
3734
|
stats.malformed++;
|
|
3503
3735
|
continue;
|
|
3504
3736
|
}
|
|
3505
|
-
const
|
|
3506
|
-
if (prev && prev.sha256 ===
|
|
3507
|
-
setState.run({ file_path:
|
|
3737
|
+
const hash2 = sha256(buf);
|
|
3738
|
+
if (prev && prev.sha256 === hash2) {
|
|
3739
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3508
3740
|
stats.skipped++;
|
|
3509
3741
|
continue;
|
|
3510
3742
|
}
|
|
@@ -3519,14 +3751,14 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3519
3751
|
stats.malformed++;
|
|
3520
3752
|
continue;
|
|
3521
3753
|
}
|
|
3522
|
-
const runId = strOrNull(d.runId) ?? basename3(
|
|
3754
|
+
const runId = strOrNull(d.runId) ?? basename3(path2, ".json");
|
|
3523
3755
|
const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
|
|
3524
3756
|
const durationMs2 = intOrNull(d.durationMs);
|
|
3525
3757
|
const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
|
|
3526
3758
|
upsert.run({
|
|
3527
3759
|
run_id: runId,
|
|
3528
3760
|
source_id: sourceId,
|
|
3529
|
-
session_id: sessionFromPath(
|
|
3761
|
+
session_id: sessionFromPath(path2),
|
|
3530
3762
|
task_id: strOrNull(d.taskId),
|
|
3531
3763
|
workflow_name: strOrNull(d.workflowName),
|
|
3532
3764
|
status: strOrNull(d.status),
|
|
@@ -3544,7 +3776,7 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
|
|
|
3544
3776
|
ended_at: endedAt,
|
|
3545
3777
|
ingested_at: now2
|
|
3546
3778
|
});
|
|
3547
|
-
setState.run({ file_path:
|
|
3779
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3548
3780
|
stats.upserted++;
|
|
3549
3781
|
}
|
|
3550
3782
|
}
|
|
@@ -3600,25 +3832,25 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3600
3832
|
source_id=excluded.source_id, agent_type=excluded.agent_type,
|
|
3601
3833
|
agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
|
|
3602
3834
|
tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
|
|
3603
|
-
for (const
|
|
3835
|
+
for (const path2 of paths) {
|
|
3604
3836
|
stats.files++;
|
|
3605
|
-
const st = statSync3(
|
|
3837
|
+
const st = statSync3(path2);
|
|
3606
3838
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3607
|
-
const prev = full ? void 0 : getState.get(
|
|
3839
|
+
const prev = full ? void 0 : getState.get(path2);
|
|
3608
3840
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3609
3841
|
stats.skipped++;
|
|
3610
3842
|
continue;
|
|
3611
3843
|
}
|
|
3612
3844
|
let buf;
|
|
3613
3845
|
try {
|
|
3614
|
-
buf = readFileSync5(
|
|
3846
|
+
buf = readFileSync5(path2);
|
|
3615
3847
|
} catch {
|
|
3616
3848
|
stats.malformed++;
|
|
3617
3849
|
continue;
|
|
3618
3850
|
}
|
|
3619
|
-
const
|
|
3620
|
-
if (prev && prev.sha256 ===
|
|
3621
|
-
setState.run({ file_path:
|
|
3851
|
+
const hash2 = sha256(buf);
|
|
3852
|
+
if (prev && prev.sha256 === hash2) {
|
|
3853
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3622
3854
|
stats.skipped++;
|
|
3623
3855
|
continue;
|
|
3624
3856
|
}
|
|
@@ -3633,7 +3865,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3633
3865
|
stats.malformed++;
|
|
3634
3866
|
continue;
|
|
3635
3867
|
}
|
|
3636
|
-
const sessionId = basename4(
|
|
3868
|
+
const sessionId = basename4(path2, ".meta.json");
|
|
3637
3869
|
upsert.run({
|
|
3638
3870
|
session_id: sessionId,
|
|
3639
3871
|
source_id: sourceId,
|
|
@@ -3643,7 +3875,7 @@ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2,
|
|
|
3643
3875
|
tool_use_id: strOrNull2(d.toolUseId),
|
|
3644
3876
|
ingested_at: now2
|
|
3645
3877
|
});
|
|
3646
|
-
setState.run({ file_path:
|
|
3878
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3647
3879
|
stats.upserted++;
|
|
3648
3880
|
}
|
|
3649
3881
|
}
|
|
@@ -3680,8 +3912,8 @@ function discoverResults(sourceArchiveDir) {
|
|
|
3680
3912
|
}
|
|
3681
3913
|
return out;
|
|
3682
3914
|
}
|
|
3683
|
-
function sessionFromPath2(
|
|
3684
|
-
const parts =
|
|
3915
|
+
function sessionFromPath2(path2) {
|
|
3916
|
+
const parts = path2.split("/");
|
|
3685
3917
|
const i = parts.indexOf("tool-results");
|
|
3686
3918
|
return i > 0 ? parts[i - 1] : null;
|
|
3687
3919
|
}
|
|
@@ -3698,40 +3930,40 @@ function ingestToolResults(db, sourceArchiveDir, excludedDirs, now2, stats, full
|
|
|
3698
3930
|
VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
|
|
3699
3931
|
ON CONFLICT(session_id, name) DO UPDATE SET
|
|
3700
3932
|
path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
|
|
3701
|
-
for (const
|
|
3702
|
-
const sessionId = sessionFromPath2(
|
|
3933
|
+
for (const path2 of paths) {
|
|
3934
|
+
const sessionId = sessionFromPath2(path2);
|
|
3703
3935
|
if (!sessionId)
|
|
3704
3936
|
continue;
|
|
3705
3937
|
stats.files++;
|
|
3706
|
-
const st = statSync4(
|
|
3938
|
+
const st = statSync4(path2);
|
|
3707
3939
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3708
|
-
const prev = full ? void 0 : getState.get(
|
|
3940
|
+
const prev = full ? void 0 : getState.get(path2);
|
|
3709
3941
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3710
3942
|
stats.skipped++;
|
|
3711
3943
|
continue;
|
|
3712
3944
|
}
|
|
3713
3945
|
let buf;
|
|
3714
3946
|
try {
|
|
3715
|
-
buf = readFileSync6(
|
|
3947
|
+
buf = readFileSync6(path2);
|
|
3716
3948
|
} catch {
|
|
3717
3949
|
stats.malformed++;
|
|
3718
3950
|
continue;
|
|
3719
3951
|
}
|
|
3720
|
-
const
|
|
3721
|
-
if (prev && prev.sha256 ===
|
|
3722
|
-
setState.run({ file_path:
|
|
3952
|
+
const hash2 = sha256(buf);
|
|
3953
|
+
if (prev && prev.sha256 === hash2) {
|
|
3954
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3723
3955
|
stats.skipped++;
|
|
3724
3956
|
continue;
|
|
3725
3957
|
}
|
|
3726
3958
|
upsert.run({
|
|
3727
3959
|
session_id: sessionId,
|
|
3728
|
-
name: basename5(
|
|
3729
|
-
path,
|
|
3960
|
+
name: basename5(path2, ".txt"),
|
|
3961
|
+
path: path2,
|
|
3730
3962
|
bytes: st.size,
|
|
3731
3963
|
text: buf.toString("utf8"),
|
|
3732
3964
|
ingested_at: now2
|
|
3733
3965
|
});
|
|
3734
|
-
setState.run({ file_path:
|
|
3966
|
+
setState.run({ file_path: path2, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
|
|
3735
3967
|
stats.upserted++;
|
|
3736
3968
|
}
|
|
3737
3969
|
}
|
|
@@ -3806,13 +4038,13 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
3806
4038
|
}
|
|
3807
4039
|
const small = st.size <= STREAM_THRESHOLD;
|
|
3808
4040
|
const buf = small ? readFileSync7(file.path) : null;
|
|
3809
|
-
const
|
|
3810
|
-
if (prev && prev.sha256 ===
|
|
4041
|
+
const hash2 = small ? sha256(buf) : sha256File(file.path);
|
|
4042
|
+
if (prev && prev.sha256 === hash2) {
|
|
3811
4043
|
stmts.setState.run({
|
|
3812
4044
|
file_path: file.path,
|
|
3813
4045
|
size: st.size,
|
|
3814
4046
|
mtime_ms: mtimeMs,
|
|
3815
|
-
sha256:
|
|
4047
|
+
sha256: hash2,
|
|
3816
4048
|
events_ingested: prev.events_ingested,
|
|
3817
4049
|
ingested_at: now2
|
|
3818
4050
|
});
|
|
@@ -3820,7 +4052,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
3820
4052
|
continue;
|
|
3821
4053
|
}
|
|
3822
4054
|
const lines = small ? buf.toString("utf8").split("\n") : streamLines(file.path);
|
|
3823
|
-
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now2, stats);
|
|
4055
|
+
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: hash2 }, now2, stats);
|
|
3824
4056
|
dirty.add(file.sessionId);
|
|
3825
4057
|
}
|
|
3826
4058
|
ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
|
|
@@ -4462,6 +4694,34 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
4462
4694
|
import Fastify from "fastify";
|
|
4463
4695
|
import fastifyStatic from "@fastify/static";
|
|
4464
4696
|
|
|
4697
|
+
// ../server/dist/export.js
|
|
4698
|
+
function parseRedactionLevel(raw) {
|
|
4699
|
+
return raw === "off" ? "off" : raw === "structure" ? "structure" : "secrets";
|
|
4700
|
+
}
|
|
4701
|
+
function renderSessionExport(db, id, level) {
|
|
4702
|
+
const result = getSession(db, id);
|
|
4703
|
+
if (!result)
|
|
4704
|
+
return null;
|
|
4705
|
+
const s = result.session;
|
|
4706
|
+
const events = result.events.map((e) => ({
|
|
4707
|
+
type: e.type,
|
|
4708
|
+
role: e.role,
|
|
4709
|
+
timestamp: e.timestamp,
|
|
4710
|
+
text: e.text,
|
|
4711
|
+
thinking: e.thinking,
|
|
4712
|
+
toolCalls: e.toolCalls.map((t) => ({
|
|
4713
|
+
tool_name: t.tool_name,
|
|
4714
|
+
skill_name: t.skill_name,
|
|
4715
|
+
agent_type: t.agent_type,
|
|
4716
|
+
input_json: t.input_json,
|
|
4717
|
+
status: t.status
|
|
4718
|
+
}))
|
|
4719
|
+
}));
|
|
4720
|
+
const { markdown } = exportMarkdown({ id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at }, events, { level });
|
|
4721
|
+
const suffix = level === "off" ? "" : ".redacted";
|
|
4722
|
+
return { markdown, filename: `session-${id.slice(0, 8)}${suffix}.md` };
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4465
4725
|
// ../server/dist/dashboard.js
|
|
4466
4726
|
function sessionWhere(f) {
|
|
4467
4727
|
const where = [];
|
|
@@ -4718,6 +4978,14 @@ function originAllowed(origin) {
|
|
|
4718
4978
|
return false;
|
|
4719
4979
|
}
|
|
4720
4980
|
}
|
|
4981
|
+
function writeBlocked(headers) {
|
|
4982
|
+
const site = headers["sec-fetch-site"];
|
|
4983
|
+
const s = Array.isArray(site) ? site[0] : site;
|
|
4984
|
+
if (typeof s === "string" && s !== "same-origin" && s !== "none")
|
|
4985
|
+
return true;
|
|
4986
|
+
const origin = headers["origin"];
|
|
4987
|
+
return !originAllowed(Array.isArray(origin) ? origin[0] : origin);
|
|
4988
|
+
}
|
|
4721
4989
|
function runRefresh() {
|
|
4722
4990
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
4723
4991
|
const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
|
|
@@ -4823,8 +5091,24 @@ function setPref(db, key, value) {
|
|
|
4823
5091
|
}
|
|
4824
5092
|
|
|
4825
5093
|
// ../server/dist/app.js
|
|
5094
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
4826
5095
|
async function createApp(db, opts = {}) {
|
|
4827
5096
|
const app = Fastify({ logger: false });
|
|
5097
|
+
if (opts.enforceLoopbackHost !== false) {
|
|
5098
|
+
app.addHook("onRequest", async (req, reply) => {
|
|
5099
|
+
const host = (req.headers.host ?? "").replace(/:\d+$/, "");
|
|
5100
|
+
if (!LOOPBACK_HOSTS.has(host)) {
|
|
5101
|
+
return reply.code(403).send({ error: { code: "FORBIDDEN_HOST", message: "non-loopback Host rejected" } });
|
|
5102
|
+
}
|
|
5103
|
+
});
|
|
5104
|
+
}
|
|
5105
|
+
app.addHook("onSend", async (_req, reply, payload) => {
|
|
5106
|
+
reply.header("X-Frame-Options", "DENY");
|
|
5107
|
+
reply.header("X-Content-Type-Options", "nosniff");
|
|
5108
|
+
reply.header("Referrer-Policy", "no-referrer");
|
|
5109
|
+
reply.header("Content-Security-Policy", "frame-ancestors 'none'");
|
|
5110
|
+
return payload;
|
|
5111
|
+
});
|
|
4828
5112
|
let triageDb = null;
|
|
4829
5113
|
if (opts.triageDbPath) {
|
|
4830
5114
|
triageDb = openTriage(opts.triageDbPath);
|
|
@@ -4833,7 +5117,7 @@ async function createApp(db, opts = {}) {
|
|
|
4833
5117
|
app.addHook("onClose", async () => triageDb?.close());
|
|
4834
5118
|
}
|
|
4835
5119
|
const guardWrite = (req, reply) => {
|
|
4836
|
-
if (
|
|
5120
|
+
if (writeBlocked(req.headers)) {
|
|
4837
5121
|
reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin write blocked" } });
|
|
4838
5122
|
return false;
|
|
4839
5123
|
}
|
|
@@ -4848,14 +5132,15 @@ async function createApp(db, opts = {}) {
|
|
|
4848
5132
|
return { ok: true, last_ingested: lastIngested(db), schema_version: s.db_version, schema_stale: s.stale };
|
|
4849
5133
|
});
|
|
4850
5134
|
app.post("/api/refresh", async (req, reply) => {
|
|
4851
|
-
if (
|
|
5135
|
+
if (writeBlocked(req.headers)) {
|
|
4852
5136
|
return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
|
|
4853
5137
|
}
|
|
4854
5138
|
let result;
|
|
4855
5139
|
try {
|
|
4856
5140
|
result = runRefresh();
|
|
4857
5141
|
} catch (e) {
|
|
4858
|
-
|
|
5142
|
+
console.error("agent-lens-server: refresh failed:", e);
|
|
5143
|
+
return reply.code(500).send({ error: { code: "REFRESH_FAILED", message: "refresh failed; see server logs" } });
|
|
4859
5144
|
}
|
|
4860
5145
|
if (!result) {
|
|
4861
5146
|
return reply.code(409).send({ error: { code: "REFRESH_IN_PROGRESS", message: "a collect/ingest run is already in progress" } });
|
|
@@ -5006,34 +5291,11 @@ async function createApp(db, opts = {}) {
|
|
|
5006
5291
|
});
|
|
5007
5292
|
app.get("/api/sessions/:id/export.md", async (req, reply) => {
|
|
5008
5293
|
const { id } = req.params;
|
|
5009
|
-
const
|
|
5010
|
-
|
|
5294
|
+
const level = parseRedactionLevel(req.query.redact);
|
|
5295
|
+
const out = renderSessionExport(db, id, level);
|
|
5296
|
+
if (!out)
|
|
5011
5297
|
return reply.code(404).send({ error: "not found" });
|
|
5012
|
-
|
|
5013
|
-
const events = result.events.map((e) => ({
|
|
5014
|
-
type: e.type,
|
|
5015
|
-
role: e.role,
|
|
5016
|
-
timestamp: e.timestamp,
|
|
5017
|
-
text: e.text,
|
|
5018
|
-
thinking: e.thinking,
|
|
5019
|
-
toolCalls: e.toolCalls.map((t) => ({
|
|
5020
|
-
tool_name: t.tool_name,
|
|
5021
|
-
skill_name: t.skill_name,
|
|
5022
|
-
agent_type: t.agent_type,
|
|
5023
|
-
input_json: t.input_json,
|
|
5024
|
-
status: t.status
|
|
5025
|
-
}))
|
|
5026
|
-
}));
|
|
5027
|
-
const md = sessionToMarkdown({
|
|
5028
|
-
id: s.id,
|
|
5029
|
-
title: s.title,
|
|
5030
|
-
source: s.source_id,
|
|
5031
|
-
project: s.project_path,
|
|
5032
|
-
model: null,
|
|
5033
|
-
started_at: s.started_at,
|
|
5034
|
-
ended_at: s.ended_at
|
|
5035
|
-
}, events);
|
|
5036
|
-
reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="session-${id.slice(0, 8)}.md"`).send(md);
|
|
5298
|
+
reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
|
|
5037
5299
|
});
|
|
5038
5300
|
if (opts.webDist && existsSync6(opts.webDist)) {
|
|
5039
5301
|
await app.register(fastifyStatic, { root: opts.webDist });
|
|
@@ -5064,7 +5326,8 @@ async function startServer() {
|
|
|
5064
5326
|
}
|
|
5065
5327
|
const triageDbPath = process.env.AGENT_LENS_TRIAGE_DB || join10(dirname5(dbPath), "triage.db");
|
|
5066
5328
|
const db = openReadonly(dbPath);
|
|
5067
|
-
const
|
|
5329
|
+
const enforceLoopbackHost = host === "127.0.0.1" || host === "localhost";
|
|
5330
|
+
const app = await createApp(db, { webDist, triageDbPath, enforceLoopbackHost });
|
|
5068
5331
|
try {
|
|
5069
5332
|
await app.listen({ host, port });
|
|
5070
5333
|
console.log(`agent-lens-server: http://${host}:${port} (db: ${dbPath})`);
|
|
@@ -5188,6 +5451,25 @@ cli.command("watch", "Watch sources and collect + ingest on change (resident pro
|
|
|
5188
5451
|
cli.command("metrics", "Re-run classification over an already-ingested DB").option("--db <path>", "SQLite DB path").action((opts) => {
|
|
5189
5452
|
runMetrics(opts.db ? ["--db", opts.db] : []);
|
|
5190
5453
|
});
|
|
5454
|
+
cli.command("export <sessionId>", "Write a session to a shareable Markdown file (redacted by default)").option("--out <file>", "Write to this file (default: stdout)").option("--level <level>", "Redaction level: secrets (default) | structure").option("--no-redact", "Verbatim, UNREDACTED output (opt-out)").option("--db <path>", "SQLite DB path").action((sessionId, opts) => {
|
|
5455
|
+
const dbPath = opts.db || process.env.AGENT_LENS_DB || join12(resolveDataDir(findRepoRoot()), "agent-lens.db");
|
|
5456
|
+
if (!existsSync9(dbPath)) {
|
|
5457
|
+
console.error(`agent-lens: db not found: ${dbPath} (run ingest first)`);
|
|
5458
|
+
process.exit(1);
|
|
5459
|
+
}
|
|
5460
|
+
const level = opts.redact === false ? "off" : parseRedactionLevel(opts.level);
|
|
5461
|
+
const out = renderSessionExport(openReadonly(dbPath), sessionId, level);
|
|
5462
|
+
if (!out) {
|
|
5463
|
+
console.error(`agent-lens: session not found: ${sessionId}`);
|
|
5464
|
+
process.exit(1);
|
|
5465
|
+
}
|
|
5466
|
+
if (opts.out) {
|
|
5467
|
+
writeFileSync3(opts.out, out.markdown);
|
|
5468
|
+
console.error(`agent-lens: wrote ${out.filename} \u2192 ${opts.out} (redaction: ${level})`);
|
|
5469
|
+
} else {
|
|
5470
|
+
process.stdout.write(out.markdown);
|
|
5471
|
+
}
|
|
5472
|
+
});
|
|
5191
5473
|
cli.command(
|
|
5192
5474
|
"service <action> [target]",
|
|
5193
5475
|
"Install/uninstall/status OS services \u2014 action: install|uninstall|status; target: collector|server|all (default all)"
|