@stroq/cli 0.12.1 → 0.13.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/index.js
CHANGED
|
@@ -1526,6 +1526,7 @@ function firstArgAfter(segment2) {
|
|
|
1526
1526
|
// ../core/src/actions/self-config.ts
|
|
1527
1527
|
var SELF_CONFIG_FILE = /(\.claude\/settings(\.local)?\.json|\.cursor\/hooks\.json|\.codex\/(hooks\.json|config\.toml)|\.github\/(hooks(?![\w.-])|copilot\/settings(\.local)?\.json)|\.copilot\/(hooks(?![\w.-])|settings\.json|config\.json)|\.openclaw\/(openclaw\.json|plugins(?![\w.-])|extensions(?![\w.-]))|(\.windsurf|\.codeium(\/windsurf)?)\/hooks\.json|(?<![\w.-])\/etc\/windsurf\/hooks\.json|Application(?:\\ | )Support\/Windsurf\/hooks\.json|(?<![\w.-])claude_desktop_config\.json|(?<![\w.-])mcp_config\.json|\.stroq(\/|\b))/;
|
|
1528
1528
|
var PROTECTED_DIRS = /\.(claude|cursor|codex|copilot|openclaw|stroq|windsurf|codeium|github\/(hooks|copilot))(\/|$|\s)/;
|
|
1529
|
+
var PROTECTED_DIR_BARE = /(^|[\s"'=(])(?:[\w.~/-]*\/)?(?:\.(claude|cursor|codex|copilot|openclaw|stroq|windsurf|codeium)|\.github\/(hooks|copilot))(?:\/\*?|\*)?(?=$|[\s"';|&)])/i;
|
|
1529
1530
|
var SELF_CONFIG_READ_COMMANDS = /* @__PURE__ */ new Set([
|
|
1530
1531
|
"cat",
|
|
1531
1532
|
"less",
|
|
@@ -1608,7 +1609,8 @@ function isFindWriteIntent(segment2) {
|
|
|
1608
1609
|
}
|
|
1609
1610
|
function touchesSelfConfig(segment2, word) {
|
|
1610
1611
|
if (SELF_CONFIG_FILE.test(segment2)) return true;
|
|
1611
|
-
|
|
1612
|
+
if (word === "find" && PROTECTED_DIRS.test(segment2)) return true;
|
|
1613
|
+
return SELF_CONFIG_WRITE_COMMANDS.has(word) && PROTECTED_DIR_BARE.test(segment2);
|
|
1612
1614
|
}
|
|
1613
1615
|
function isSelfConfigWriteIntent(segment2, word) {
|
|
1614
1616
|
if (SELF_CONFIG_WRITE_COMMANDS.has(word)) return true;
|
|
@@ -1924,7 +1926,7 @@ function classifyCommand(command, cwd) {
|
|
|
1924
1926
|
}
|
|
1925
1927
|
|
|
1926
1928
|
// ../core/src/actions/classify-tool.ts
|
|
1927
|
-
var SECRET_PATH = /(\/\.ssh
|
|
1929
|
+
var SECRET_PATH = /(\/\.ssh(\/|$)|\bid_(rsa|ed25519|ecdsa|dsa)\b|\/\.aws(\/|$)|(^|\/)\.env(\.[\w-]+)?$|\.(pem|p12|pfx|key)$|\/\.(npmrc|netrc|pgpass|git-credentials)$|\/\.kube(\/config$|$)|\/\.config\/gcloud(\/|$)|\/etc\/(shadow|passwd)$)/;
|
|
1928
1930
|
var SIDE_EFFECT_TOOL = /(send|post|publish|upload|email|mail|message|notify|pay|transfer|purchase|delete|remove|drop|deploy|execute|exec|run|shell|write|update|create|comment|merge|push)/i;
|
|
1929
1931
|
var WRITE_SHAPED_TOOL = /(write|edit|delete|remove|move|rename|append|put|save|update|create_file|mkdir)/i;
|
|
1930
1932
|
var PATH_LIKE_KEY = /^(path|filepath|file_path|file|target|dest|destination|uri|url)s?$/i;
|
|
@@ -1941,7 +1943,20 @@ function pathOf(toolInput) {
|
|
|
1941
1943
|
const candidate = toolInput["file_path"] ?? toolInput["notebook_path"] ?? toolInput["path"] ?? "";
|
|
1942
1944
|
return typeof candidate === "string" ? candidate : "";
|
|
1943
1945
|
}
|
|
1944
|
-
function
|
|
1946
|
+
function normalizePathForMatch(path) {
|
|
1947
|
+
const collapsed = path.replace(/(^|\/)\.(?=\/)/g, "$1").replace(/\/{2,}/g, "/");
|
|
1948
|
+
const segments = [];
|
|
1949
|
+
for (const segment2 of collapsed.split("/")) {
|
|
1950
|
+
if (segment2 === ".." && segments.length > 0 && segments[segments.length - 1] !== "..") {
|
|
1951
|
+
segments.pop();
|
|
1952
|
+
} else {
|
|
1953
|
+
segments.push(segment2);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return segments.join("/").toLowerCase();
|
|
1957
|
+
}
|
|
1958
|
+
function classifyPath(rawPath, write2) {
|
|
1959
|
+
const path = normalizePathForMatch(rawPath);
|
|
1945
1960
|
const classes = [];
|
|
1946
1961
|
const signals = [];
|
|
1947
1962
|
if (write2 && SELF_CONFIG_FILE.test(path)) {
|
|
@@ -2593,8 +2608,14 @@ function pipeShellAtoms(text) {
|
|
|
2593
2608
|
atom: { kind: "pipe_shell", value: collapse(m[0]) }
|
|
2594
2609
|
}));
|
|
2595
2610
|
}
|
|
2611
|
+
var PATH_SEGMENT_MAX = 16;
|
|
2612
|
+
function looksLikePath(body) {
|
|
2613
|
+
const parts = body.split("/");
|
|
2614
|
+
return parts.length >= 3 && parts.every((part) => part.length <= PATH_SEGMENT_MAX);
|
|
2615
|
+
}
|
|
2596
2616
|
function looksBase64(token) {
|
|
2597
2617
|
const body = token.replace(/=+$/, "");
|
|
2618
|
+
if (looksLikePath(body)) return false;
|
|
2598
2619
|
return !HEX.test(body) && /[a-z]/.test(body) && /[A-Z]/.test(body) && /[0-9+/]/.test(body);
|
|
2599
2620
|
}
|
|
2600
2621
|
function encodedAtoms(text) {
|
|
@@ -6602,8 +6623,8 @@ var MAX_CONFIG_BYTES = 256 * 1024;
|
|
|
6602
6623
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".venv", "venv", "vendor", "target", "dist", "build"]);
|
|
6603
6624
|
var readTextOr = (file, fallback = "") => {
|
|
6604
6625
|
try {
|
|
6605
|
-
const
|
|
6606
|
-
if (!
|
|
6626
|
+
const stat3 = statSync8(file);
|
|
6627
|
+
if (!stat3.isFile() || stat3.size > MAX_CONFIG_BYTES) return fallback;
|
|
6607
6628
|
return readFileSync15(file, "utf8");
|
|
6608
6629
|
} catch {
|
|
6609
6630
|
return fallback;
|
|
@@ -6664,8 +6685,8 @@ function gitHookHits(root) {
|
|
|
6664
6685
|
try {
|
|
6665
6686
|
return readdirSync6(dir).filter((name) => !name.endsWith(".sample")).filter((name) => {
|
|
6666
6687
|
try {
|
|
6667
|
-
const
|
|
6668
|
-
return
|
|
6688
|
+
const stat3 = statSync8(join19(dir, name));
|
|
6689
|
+
return stat3.isFile() && (stat3.mode & 73) !== 0;
|
|
6669
6690
|
} catch {
|
|
6670
6691
|
return false;
|
|
6671
6692
|
}
|
|
@@ -8426,10 +8447,397 @@ ${MCP_USAGE}`);
|
|
|
8426
8447
|
});
|
|
8427
8448
|
}
|
|
8428
8449
|
|
|
8450
|
+
// src/commands/replay.ts
|
|
8451
|
+
import { mkdtemp as mkdtemp2, rm as rm5 } from "fs/promises";
|
|
8452
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
8453
|
+
import { join as join21 } from "path";
|
|
8454
|
+
import { parseArgs as parseArgs8 } from "util";
|
|
8455
|
+
|
|
8456
|
+
// src/replay/transcript.ts
|
|
8457
|
+
import { readdir, readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
8458
|
+
import { homedir as homedir17 } from "os";
|
|
8459
|
+
import { join as join20 } from "path";
|
|
8460
|
+
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8461
|
+
function resultText(content) {
|
|
8462
|
+
if (typeof content === "string") return content;
|
|
8463
|
+
if (!Array.isArray(content)) return "";
|
|
8464
|
+
return content.map((b) => {
|
|
8465
|
+
if (typeof b === "string") return b;
|
|
8466
|
+
if (isRecord2(b) && typeof b["text"] === "string") return b["text"];
|
|
8467
|
+
return "";
|
|
8468
|
+
}).filter((t) => t.length > 0).join("\n");
|
|
8469
|
+
}
|
|
8470
|
+
function blocksOf(record) {
|
|
8471
|
+
const message = record["message"];
|
|
8472
|
+
if (!isRecord2(message)) return [];
|
|
8473
|
+
const content = message["content"];
|
|
8474
|
+
return Array.isArray(content) ? content.filter(isRecord2) : [];
|
|
8475
|
+
}
|
|
8476
|
+
function parseTranscript(text) {
|
|
8477
|
+
const events = [];
|
|
8478
|
+
const pending = /* @__PURE__ */ new Map();
|
|
8479
|
+
let sessionId = "transcript";
|
|
8480
|
+
let cwd = null;
|
|
8481
|
+
let skipped = 0;
|
|
8482
|
+
for (const line2 of text.split("\n")) {
|
|
8483
|
+
if (line2.trim().length === 0) continue;
|
|
8484
|
+
let record;
|
|
8485
|
+
try {
|
|
8486
|
+
record = JSON.parse(line2);
|
|
8487
|
+
} catch {
|
|
8488
|
+
skipped += 1;
|
|
8489
|
+
continue;
|
|
8490
|
+
}
|
|
8491
|
+
if (!isRecord2(record)) {
|
|
8492
|
+
skipped += 1;
|
|
8493
|
+
continue;
|
|
8494
|
+
}
|
|
8495
|
+
if (typeof record["sessionId"] === "string") sessionId = record["sessionId"];
|
|
8496
|
+
if (cwd === null && typeof record["cwd"] === "string") cwd = record["cwd"];
|
|
8497
|
+
const at = typeof record["timestamp"] === "string" ? record["timestamp"] : "";
|
|
8498
|
+
for (const block of blocksOf(record)) {
|
|
8499
|
+
if (block.type === "tool_use" && typeof block.id === "string") {
|
|
8500
|
+
const tool = typeof block.name === "string" ? block.name : "unknown";
|
|
8501
|
+
const input = isRecord2(block.input) ? block.input : {};
|
|
8502
|
+
pending.set(block.id, { tool, input });
|
|
8503
|
+
events.push({ kind: "pre", id: block.id, tool, input, at });
|
|
8504
|
+
continue;
|
|
8505
|
+
}
|
|
8506
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
8507
|
+
const call = pending.get(block.tool_use_id);
|
|
8508
|
+
if (!call) continue;
|
|
8509
|
+
events.push({
|
|
8510
|
+
kind: "post",
|
|
8511
|
+
id: block.tool_use_id,
|
|
8512
|
+
tool: call.tool,
|
|
8513
|
+
input: call.input,
|
|
8514
|
+
resultText: resultText(block.content),
|
|
8515
|
+
at
|
|
8516
|
+
});
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
return { sessionId, cwd, events, skipped };
|
|
8521
|
+
}
|
|
8522
|
+
function projectSlug(cwd) {
|
|
8523
|
+
return cwd.replace(/[/.]/g, "-");
|
|
8524
|
+
}
|
|
8525
|
+
var transcriptRoot = () => join20(homedir17(), ".claude", "projects");
|
|
8526
|
+
async function findTranscripts(cwd) {
|
|
8527
|
+
const root = transcriptRoot();
|
|
8528
|
+
const scoped = join20(root, projectSlug(cwd));
|
|
8529
|
+
const found = await listJsonl(scoped) ?? [];
|
|
8530
|
+
if (found.length > 0) return found.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8531
|
+
let dirs;
|
|
8532
|
+
try {
|
|
8533
|
+
dirs = (await readdir(root, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => join20(root, d.name));
|
|
8534
|
+
} catch {
|
|
8535
|
+
return [];
|
|
8536
|
+
}
|
|
8537
|
+
const all = [];
|
|
8538
|
+
for (const dir of dirs) all.push(...await listJsonl(dir) ?? []);
|
|
8539
|
+
return all.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8540
|
+
}
|
|
8541
|
+
async function listJsonl(dir) {
|
|
8542
|
+
let names;
|
|
8543
|
+
try {
|
|
8544
|
+
names = await readdir(dir);
|
|
8545
|
+
} catch {
|
|
8546
|
+
return null;
|
|
8547
|
+
}
|
|
8548
|
+
const files = [];
|
|
8549
|
+
for (const name of names) {
|
|
8550
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
8551
|
+
const path = join20(dir, name);
|
|
8552
|
+
try {
|
|
8553
|
+
files.push({ path, mtimeMs: (await stat2(path)).mtimeMs });
|
|
8554
|
+
} catch {
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
return files;
|
|
8558
|
+
}
|
|
8559
|
+
async function readTranscript(path) {
|
|
8560
|
+
return parseTranscript(await readFile5(path, "utf8"));
|
|
8561
|
+
}
|
|
8562
|
+
|
|
8563
|
+
// src/commands/replay.ts
|
|
8564
|
+
var CLIP = /…+$/;
|
|
8565
|
+
var sourceKey = (tool, source) => JSON.stringify([tool, source]);
|
|
8566
|
+
var ATOM_RANK = {
|
|
8567
|
+
pipe_shell: 4,
|
|
8568
|
+
encoded: 3,
|
|
8569
|
+
pkg: 2,
|
|
8570
|
+
url: 1,
|
|
8571
|
+
host: 0
|
|
8572
|
+
};
|
|
8573
|
+
var atomRank = (ev) => ATOM_RANK[ev.kind] ?? 0;
|
|
8574
|
+
function sameSource(summary, source) {
|
|
8575
|
+
const a = summary.replace(CLIP, "");
|
|
8576
|
+
const b = source.replace(CLIP, "");
|
|
8577
|
+
if (a.length === 0 || b.length === 0) return false;
|
|
8578
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
8579
|
+
}
|
|
8580
|
+
function findRead(entries, ev) {
|
|
8581
|
+
const candidates = entries.filter(
|
|
8582
|
+
(e) => e.phase === "post" && e.tool === ev.tool && sameSource(e.summary, ev.source)
|
|
8583
|
+
);
|
|
8584
|
+
if (candidates.length === 0) return null;
|
|
8585
|
+
const at = Date.parse(ev.at);
|
|
8586
|
+
const before = candidates.filter((e) => Date.parse(e.ts) <= at + 1e3);
|
|
8587
|
+
return (before.length > 0 ? before : candidates)[(before.length > 0 ? before : candidates).length - 1];
|
|
8588
|
+
}
|
|
8589
|
+
function taintingReads(entries) {
|
|
8590
|
+
return entries.filter((e) => e.phase === "post" && e.scan?.verdict === "suspect");
|
|
8591
|
+
}
|
|
8592
|
+
function buildReplay(allEntries, sessionId) {
|
|
8593
|
+
const entries = allEntries.filter((e) => e.sessionId === sessionId);
|
|
8594
|
+
const actions = entries.filter((e) => e.phase === "pre");
|
|
8595
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
8596
|
+
const ensure = (tool, source, at, suspect) => {
|
|
8597
|
+
const key = sourceKey(tool, source);
|
|
8598
|
+
const existing = bySource.get(key);
|
|
8599
|
+
if (existing) return existing.list;
|
|
8600
|
+
const list = [];
|
|
8601
|
+
bySource.set(key, {
|
|
8602
|
+
source: {
|
|
8603
|
+
tool,
|
|
8604
|
+
source,
|
|
8605
|
+
at,
|
|
8606
|
+
suspect,
|
|
8607
|
+
read: findRead(entries, { tool, source, at, suspect, kind: "url", excerpt: "" }),
|
|
8608
|
+
consequences: list
|
|
8609
|
+
},
|
|
8610
|
+
list
|
|
8611
|
+
});
|
|
8612
|
+
return list;
|
|
8613
|
+
};
|
|
8614
|
+
for (const read3 of taintingReads(entries)) {
|
|
8615
|
+
ensure(read3.tool, read3.summary, read3.ts, true);
|
|
8616
|
+
}
|
|
8617
|
+
for (const action of actions) {
|
|
8618
|
+
for (const ev of action.provenance ?? []) {
|
|
8619
|
+
ensure(ev.tool, ev.source, ev.at, ev.suspect).push({ action, evidence: ev, alsoCarried: 0 });
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
for (const group of bySource.values()) {
|
|
8623
|
+
const byAction = /* @__PURE__ */ new Map();
|
|
8624
|
+
for (const c of group.list) {
|
|
8625
|
+
byAction.set(c.action.seq, [...byAction.get(c.action.seq) ?? [], c]);
|
|
8626
|
+
}
|
|
8627
|
+
const merged = [...byAction.values()].map((carried) => {
|
|
8628
|
+
const best = carried.reduce((a, b) => atomRank(b.evidence) > atomRank(a.evidence) ? b : a);
|
|
8629
|
+
return { ...best, alsoCarried: carried.length - 1 };
|
|
8630
|
+
});
|
|
8631
|
+
group.list.length = 0;
|
|
8632
|
+
group.list.push(...merged.sort((a, b) => a.action.seq - b.action.seq));
|
|
8633
|
+
}
|
|
8634
|
+
const linked = new Set([...bySource.values()].flatMap((g) => g.list.map((c) => c.action.seq)));
|
|
8635
|
+
const secretActions = actions.filter((a) => (a.secrets ?? []).length > 0 && !linked.has(a.seq));
|
|
8636
|
+
const secretSeqs = new Set(secretActions.map((a) => a.seq));
|
|
8637
|
+
const unlinked = actions.filter((a) => !linked.has(a.seq) && !secretSeqs.has(a.seq));
|
|
8638
|
+
const sources = [...bySource.values()].map((g) => ({ ...g.source, consequences: g.list })).sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
8639
|
+
return {
|
|
8640
|
+
sessionId,
|
|
8641
|
+
total: entries.length,
|
|
8642
|
+
first: entries[0]?.ts ?? null,
|
|
8643
|
+
last: entries[entries.length - 1]?.ts ?? null,
|
|
8644
|
+
sources,
|
|
8645
|
+
secretActions,
|
|
8646
|
+
unlinked,
|
|
8647
|
+
denied: actions.filter((a) => a.decision?.effect === "deny").length,
|
|
8648
|
+
asked: actions.filter((a) => a.decision?.effect === "ask").length
|
|
8649
|
+
};
|
|
8650
|
+
}
|
|
8651
|
+
var RULE_PREVIEW = 3;
|
|
8652
|
+
function verdictTag(entry2) {
|
|
8653
|
+
const d = entry2.decision;
|
|
8654
|
+
if (!d) return "";
|
|
8655
|
+
if (d.effect === "deny") return `DENIED ${d.ruleId ?? "default"}`;
|
|
8656
|
+
if (d.effect === "ask") return `ASKED ${d.ruleId ?? "default"}`;
|
|
8657
|
+
return `allowed ${d.ruleId ?? "default"}`;
|
|
8658
|
+
}
|
|
8659
|
+
function readLine(src) {
|
|
8660
|
+
const scan = src.read?.scan;
|
|
8661
|
+
if (!scan) {
|
|
8662
|
+
return src.suspect ? "flagged suspicious" : "no rule matched it, but tool output is data, not instructions";
|
|
8663
|
+
}
|
|
8664
|
+
if (scan.verdict !== "suspect") {
|
|
8665
|
+
return `clean \u2014 no rule matched it, but tool output is data, not instructions`;
|
|
8666
|
+
}
|
|
8667
|
+
const ids = scan.ruleIds.slice(0, RULE_PREVIEW).join(", ");
|
|
8668
|
+
const more = scan.ruleIds.length > RULE_PREVIEW ? `, +${scan.ruleIds.length - RULE_PREVIEW}` : "";
|
|
8669
|
+
const waived = scan.trusted === true ? " \u2014 waived by stroq trust" : "";
|
|
8670
|
+
return `SUSPECT ${scan.score.toFixed(2)} \u2014 ${scan.ruleIds.length} rules: ${ids}${more}${waived}`;
|
|
8671
|
+
}
|
|
8672
|
+
var secretLine = (s) => `${s.name} from ${s.source}${s.canary ? " (canary)" : ""}`;
|
|
8673
|
+
function consequenceBlock(c, last) {
|
|
8674
|
+
const elbow = last ? " \u2514\u2500\u25BA" : " \u251C\u2500\u25BA";
|
|
8675
|
+
const rail = last ? " " : " \u2502 ";
|
|
8676
|
+
const gap = ageLabel(c.evidence.at, new Date(c.action.ts));
|
|
8677
|
+
return [
|
|
8678
|
+
`${elbow} #${c.action.seq} ${c.action.tool} ${c.action.summary}`,
|
|
8679
|
+
`${rail} ${verdictTag(c.action)}${" ".repeat(Math.max(1, 34 - verdictTag(c.action).length))}${gap} later`,
|
|
8680
|
+
`${rail} carried over: "${c.evidence.excerpt}" (${c.evidence.kind})` + (c.alsoCarried > 0 ? ` and ${c.alsoCarried} more` : "")
|
|
8681
|
+
];
|
|
8682
|
+
}
|
|
8683
|
+
function sourceBlock(src) {
|
|
8684
|
+
const seq = src.read ? `#${src.read.seq} ` : "";
|
|
8685
|
+
const head = [` \u25A0 ${seq}${src.tool} ${src.source}`, ` ${readLine(src)}`];
|
|
8686
|
+
if (src.consequences.length === 0) {
|
|
8687
|
+
return [...head, " nothing traced back to it", ""];
|
|
8688
|
+
}
|
|
8689
|
+
const body = src.consequences.flatMap(
|
|
8690
|
+
(c, i) => consequenceBlock(c, i === src.consequences.length - 1)
|
|
8691
|
+
);
|
|
8692
|
+
return [...head, " \u2502", ...body, ""];
|
|
8693
|
+
}
|
|
8694
|
+
function duration(model) {
|
|
8695
|
+
if (!model.first || !model.last) return "";
|
|
8696
|
+
return ` \xB7 ${ageLabel(model.first, new Date(model.last))} long`;
|
|
8697
|
+
}
|
|
8698
|
+
function formatReplay(model) {
|
|
8699
|
+
if (model.total === 0) {
|
|
8700
|
+
return `no audit entries for session ${model.sessionId}
|
|
8701
|
+
`;
|
|
8702
|
+
}
|
|
8703
|
+
const traced = new Set(model.sources.flatMap((s) => s.consequences.map((c) => c.action.seq))).size;
|
|
8704
|
+
const lines = [
|
|
8705
|
+
"stroq replay \u2014 what the agent did, and what told it to do it",
|
|
8706
|
+
"",
|
|
8707
|
+
` session ${model.sessionId} \xB7 ${model.total} events${duration(model)} \xB7 ${model.denied} denied \xB7 ${model.asked} asked`,
|
|
8708
|
+
""
|
|
8709
|
+
];
|
|
8710
|
+
if (model.sources.length > 0) {
|
|
8711
|
+
lines.push("CONTENT THE AGENT READ, AND WHAT CAME OUT OF IT", "");
|
|
8712
|
+
for (const src of model.sources) lines.push(...sourceBlock(src));
|
|
8713
|
+
}
|
|
8714
|
+
if (model.secretActions.length > 0) {
|
|
8715
|
+
lines.push("ACTIONS CARRYING A KNOWN SECRET VALUE", "");
|
|
8716
|
+
for (const a of model.secretActions) {
|
|
8717
|
+
lines.push(` \u25CF #${a.seq} ${a.tool} ${a.summary}`);
|
|
8718
|
+
lines.push(` ${verdictTag(a)}`);
|
|
8719
|
+
for (const s of a.secrets ?? []) lines.push(` ${secretLine(s)}`);
|
|
8720
|
+
lines.push("");
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
if (model.unlinked.length > 0) {
|
|
8724
|
+
lines.push(`ACTIONS WITH NO UNTRUSTED ORIGIN (${model.unlinked.length})`, "");
|
|
8725
|
+
for (const a of model.unlinked) {
|
|
8726
|
+
const tag = a.decision ? a.decision.effect : "-";
|
|
8727
|
+
lines.push(` \u25CB #${a.seq} ${a.tool} ${a.summary}`);
|
|
8728
|
+
lines.push(` ${tag}`);
|
|
8729
|
+
}
|
|
8730
|
+
lines.push("");
|
|
8731
|
+
}
|
|
8732
|
+
const judged = traced + model.secretActions.length + model.unlinked.length;
|
|
8733
|
+
lines.push(
|
|
8734
|
+
traced === 0 ? "No action in this session traced back to content the agent read." : `${traced} of ${judged} judged actions traced back to content the agent read.`
|
|
8735
|
+
);
|
|
8736
|
+
return `${lines.join("\n")}
|
|
8737
|
+
`;
|
|
8738
|
+
}
|
|
8739
|
+
function sessionsIn(entries) {
|
|
8740
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
8741
|
+
for (const e of entries) lastSeen.set(e.sessionId, e.seq);
|
|
8742
|
+
return [...lastSeen.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
|
|
8743
|
+
}
|
|
8744
|
+
async function replayTranscript(transcript) {
|
|
8745
|
+
const root = await mkdtemp2(join21(tmpdir2(), "stroq-replay-"));
|
|
8746
|
+
try {
|
|
8747
|
+
const home = join21(root, "home");
|
|
8748
|
+
const engine = createEngineAt({
|
|
8749
|
+
home,
|
|
8750
|
+
userHome: join21(root, "user"),
|
|
8751
|
+
policy: loadPolicy(),
|
|
8752
|
+
env: {}
|
|
8753
|
+
});
|
|
8754
|
+
const cwd = transcript.cwd ?? process.cwd();
|
|
8755
|
+
for (const ev of transcript.events) {
|
|
8756
|
+
const base = {
|
|
8757
|
+
sessionId: transcript.sessionId,
|
|
8758
|
+
toolName: ev.tool,
|
|
8759
|
+
toolInput: ev.input,
|
|
8760
|
+
cwd
|
|
8761
|
+
};
|
|
8762
|
+
try {
|
|
8763
|
+
if (ev.kind === "pre") await engine.pre(base);
|
|
8764
|
+
else await engine.post({ ...base, toolResultText: ev.resultText });
|
|
8765
|
+
} catch {
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8768
|
+
return await new AuditLog(auditFileIn(home)).readAll();
|
|
8769
|
+
} finally {
|
|
8770
|
+
await rm5(root, { recursive: true, force: true });
|
|
8771
|
+
}
|
|
8772
|
+
}
|
|
8773
|
+
async function runReplay(args) {
|
|
8774
|
+
const { values, positionals } = parseArgs8({
|
|
8775
|
+
args: [...args],
|
|
8776
|
+
options: {
|
|
8777
|
+
json: { type: "boolean" },
|
|
8778
|
+
list: { type: "boolean" },
|
|
8779
|
+
transcript: { type: "string" },
|
|
8780
|
+
last: { type: "boolean" }
|
|
8781
|
+
},
|
|
8782
|
+
allowPositionals: true
|
|
8783
|
+
});
|
|
8784
|
+
if (values.transcript !== void 0 || values.last === true) {
|
|
8785
|
+
const path = values.transcript ?? (await findTranscripts(process.cwd()))[0]?.path;
|
|
8786
|
+
if (path === void 0) {
|
|
8787
|
+
process.stdout.write("no agent transcript found \u2014 looked under ~/.claude/projects\n");
|
|
8788
|
+
return 1;
|
|
8789
|
+
}
|
|
8790
|
+
const transcript = await readTranscript(path);
|
|
8791
|
+
if (transcript.events.length === 0) {
|
|
8792
|
+
process.stdout.write(`no tool calls recorded in ${path}
|
|
8793
|
+
`);
|
|
8794
|
+
return 1;
|
|
8795
|
+
}
|
|
8796
|
+
const replayed = await replayTranscript(transcript);
|
|
8797
|
+
const model2 = buildReplay(replayed, transcript.sessionId);
|
|
8798
|
+
if (values.json === true) {
|
|
8799
|
+
process.stdout.write(`${JSON.stringify(model2, null, 2)}
|
|
8800
|
+
`);
|
|
8801
|
+
return 0;
|
|
8802
|
+
}
|
|
8803
|
+
process.stdout.write(`${formatReplay(model2)}
|
|
8804
|
+
replayed from ${path}
|
|
8805
|
+
`);
|
|
8806
|
+
return 0;
|
|
8807
|
+
}
|
|
8808
|
+
const entries = await new AuditLog(auditFile()).readAll();
|
|
8809
|
+
const sessions = sessionsIn(entries);
|
|
8810
|
+
if (values.list === true) {
|
|
8811
|
+
if (sessions.length === 0) {
|
|
8812
|
+
process.stdout.write("no audit entries yet\n");
|
|
8813
|
+
return 0;
|
|
8814
|
+
}
|
|
8815
|
+
for (const id of sessions) {
|
|
8816
|
+
const n = entries.filter((e) => e.sessionId === id).length;
|
|
8817
|
+
process.stdout.write(`${id} ${n} events
|
|
8818
|
+
`);
|
|
8819
|
+
}
|
|
8820
|
+
return 0;
|
|
8821
|
+
}
|
|
8822
|
+
const sessionId = positionals[0] ?? sessions[0];
|
|
8823
|
+
if (sessionId === void 0) {
|
|
8824
|
+
process.stdout.write("no audit entries yet\n");
|
|
8825
|
+
return 1;
|
|
8826
|
+
}
|
|
8827
|
+
const model = buildReplay(entries, sessionId);
|
|
8828
|
+
if (values.json === true) {
|
|
8829
|
+
process.stdout.write(`${JSON.stringify(model, null, 2)}
|
|
8830
|
+
`);
|
|
8831
|
+
return 0;
|
|
8832
|
+
}
|
|
8833
|
+
process.stdout.write(formatReplay(model));
|
|
8834
|
+
return model.total === 0 ? 1 : 0;
|
|
8835
|
+
}
|
|
8836
|
+
|
|
8429
8837
|
// src/commands/trust.ts
|
|
8430
8838
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync16, writeFileSync as writeFileSync3 } from "fs";
|
|
8431
8839
|
import { dirname as dirname7, resolve as resolve7 } from "path";
|
|
8432
|
-
import { parseArgs as
|
|
8840
|
+
import { parseArgs as parseArgs9 } from "util";
|
|
8433
8841
|
function read2(file) {
|
|
8434
8842
|
try {
|
|
8435
8843
|
return parseTrustList(readFileSync16(file, "utf8"));
|
|
@@ -8484,7 +8892,7 @@ function formatList(entries) {
|
|
|
8484
8892
|
`;
|
|
8485
8893
|
}
|
|
8486
8894
|
function runTrust(argv, now = () => /* @__PURE__ */ new Date()) {
|
|
8487
|
-
const { values, positionals } =
|
|
8895
|
+
const { values, positionals } = parseArgs9({
|
|
8488
8896
|
args: [...argv],
|
|
8489
8897
|
options: { list: { type: "boolean" }, remove: { type: "string" }, json: { type: "boolean" } },
|
|
8490
8898
|
allowPositionals: true
|
|
@@ -8519,17 +8927,17 @@ function runTrust(argv, now = () => /* @__PURE__ */ new Date()) {
|
|
|
8519
8927
|
|
|
8520
8928
|
// src/commands/untaint.ts
|
|
8521
8929
|
import { existsSync as existsSync14, readdirSync as readdirSync7 } from "fs";
|
|
8522
|
-
import { rm as
|
|
8523
|
-
import { join as
|
|
8524
|
-
import { parseArgs as
|
|
8930
|
+
import { rm as rm6 } from "fs/promises";
|
|
8931
|
+
import { join as join22 } from "path";
|
|
8932
|
+
import { parseArgs as parseArgs10 } from "util";
|
|
8525
8933
|
var USAGE = "usage: stroq untaint --session <id> | --all\n";
|
|
8526
8934
|
async function clearAll() {
|
|
8527
8935
|
const dir = sessionsDir();
|
|
8528
8936
|
if (!existsSync14(dir)) return;
|
|
8529
|
-
await Promise.all(readdirSync7(dir).map((f) =>
|
|
8937
|
+
await Promise.all(readdirSync7(dir).map((f) => rm6(join22(dir, f), { force: true })));
|
|
8530
8938
|
}
|
|
8531
8939
|
async function runUntaint(args) {
|
|
8532
|
-
const { values } =
|
|
8940
|
+
const { values } = parseArgs10({
|
|
8533
8941
|
args: [...args],
|
|
8534
8942
|
options: {
|
|
8535
8943
|
session: { type: "string" },
|
|
@@ -8566,7 +8974,7 @@ async function runVerify() {
|
|
|
8566
8974
|
}
|
|
8567
8975
|
|
|
8568
8976
|
// src/commands/why.ts
|
|
8569
|
-
import { parseArgs as
|
|
8977
|
+
import { parseArgs as parseArgs11 } from "util";
|
|
8570
8978
|
function verdictLine(entry2) {
|
|
8571
8979
|
if (entry2.decision) {
|
|
8572
8980
|
return `${entry2.decision.effect} by ${entry2.decision.ruleId ?? "default"}: ${entry2.decision.reason}`;
|
|
@@ -8599,7 +9007,7 @@ function formatWhy(entry2, state, now) {
|
|
|
8599
9007
|
`;
|
|
8600
9008
|
}
|
|
8601
9009
|
async function runWhy(args) {
|
|
8602
|
-
const { values } =
|
|
9010
|
+
const { values } = parseArgs11({ args: [...args], options: { seq: { type: "string" } } });
|
|
8603
9011
|
const entries = await new AuditLog(auditFile()).readAll();
|
|
8604
9012
|
const seq = values.seq === void 0 ? null : Number.parseInt(values.seq, 10);
|
|
8605
9013
|
const target = seq === null ? [...entries].reverse().find((e) => e.decision !== void 0 && e.decision.effect !== "allow") : entries.find((e) => e.seq === seq);
|
|
@@ -8632,6 +9040,10 @@ Commands:
|
|
|
8632
9040
|
verify verify the audit hash chain
|
|
8633
9041
|
untaint [--session <id>] [--all] clear a false-positive session's taint, or every session's
|
|
8634
9042
|
why [--seq <n>] explain the most recent denied/asked action: rule, provenance, taint
|
|
9043
|
+
replay [<session>] [--last] [--transcript <path>] [--json] [--list]
|
|
9044
|
+
rebuild a session's causal history: which content the agent read,
|
|
9045
|
+
and which actions came out of it. --last reads the agent's own
|
|
9046
|
+
transcript, so it works on sessions that ran before you installed
|
|
8635
9047
|
canary [--name <NAME>] print a canary secret to plant; its outbound use is denied and taints the session
|
|
8636
9048
|
attack [--json] [--only <id>] [--fuzz]
|
|
8637
9049
|
replay recorded incidents against your policy; exit 1 if any gets
|
|
@@ -8677,6 +9089,8 @@ async function main(argv) {
|
|
|
8677
9089
|
return runTrust(rest);
|
|
8678
9090
|
case "untaint":
|
|
8679
9091
|
return runUntaint(rest);
|
|
9092
|
+
case "replay":
|
|
9093
|
+
return runReplay(rest);
|
|
8680
9094
|
case "why":
|
|
8681
9095
|
return runWhy(rest);
|
|
8682
9096
|
case "canary":
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "stroq",
|
|
3
3
|
"name": "Stroq",
|
|
4
4
|
"description": "Local action firewall for OpenClaw: scans what the agent reads, taints the session, blocks or asks before dangerous tool calls.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.13.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED