@windyroad/risk-scorer 0.18.10 → 0.18.11
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,6 +29,10 @@ function riskDir(sessionId) {
|
|
|
28
29
|
return join(process.env.TMPDIR || "/tmp", `claude-risk-${sessionId}`);
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function pendingDir() {
|
|
33
|
+
return join(process.env.TMPDIR || "/tmp", "claude-risk-pending");
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
function statePath(input, target, suffix = "") {
|
|
32
37
|
return join(riskDir(input.session_id), `codex-agent-${Buffer.from(target).toString("base64url")}${suffix}`);
|
|
33
38
|
}
|
|
@@ -98,6 +103,63 @@ function pipelineAssessment(output) {
|
|
|
98
103
|
return { root, output: sanitized };
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
function checkoutId(root) {
|
|
107
|
+
const stat = statSync(root);
|
|
108
|
+
return createHash("sha256").update(`${stat.dev}:${stat.ino}`).digest("hex");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function stateHash(root) {
|
|
112
|
+
const state = spawnSync(join(hookDir, "lib/pipeline-state.sh"), ["--hash-inputs"], {
|
|
113
|
+
cwd: root,
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
});
|
|
116
|
+
if (state.status !== 0) return null;
|
|
117
|
+
return createHash("md5").update(state.stdout).digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function completionId(input, output) {
|
|
121
|
+
if (typeof input.session_id !== "string" || typeof input.agent_id !== "string") return null;
|
|
122
|
+
return createHash("sha256").update(`${input.session_id}\0${input.agent_id}\0${output}`).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pendingPath(id, hash, completion, suffix = "") {
|
|
126
|
+
return join(pendingDir(), `${id}-${hash}-${completion}${suffix}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function freshReceipt(path) {
|
|
130
|
+
if (!existsSync(path)) return false;
|
|
131
|
+
const ttl = Number.parseInt(process.env.RISK_TTL || "3600", 10) * 1000;
|
|
132
|
+
return Date.now() - statSync(path).mtimeMs < ttl;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function persistPendingPipeline(input) {
|
|
136
|
+
if (input.agent_type !== "wr-risk-scorer:pipeline") return;
|
|
137
|
+
const assessment = pipelineAssessment(input.last_assistant_message);
|
|
138
|
+
if (!assessment) return;
|
|
139
|
+
const id = checkoutId(assessment.root);
|
|
140
|
+
const hash = stateHash(assessment.root);
|
|
141
|
+
const completion = completionId(input, assessment.output);
|
|
142
|
+
if (!hash || !completion || !/^RISK_SCORES: commit=\d+ push=\d+ release=\d+$/m.test(assessment.output)) return;
|
|
143
|
+
mkdirSync(pendingDir(), { recursive: true });
|
|
144
|
+
const path = pendingPath(id, hash, completion);
|
|
145
|
+
for (const candidate of [path, `${path}.done`]) {
|
|
146
|
+
if (freshReceipt(candidate)) return;
|
|
147
|
+
rmSync(candidate, { force: true });
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(path, JSON.stringify({
|
|
151
|
+
role: input.agent_type,
|
|
152
|
+
output: assessment.output,
|
|
153
|
+
checkoutId: id,
|
|
154
|
+
stateHash: hash,
|
|
155
|
+
completionId: completion,
|
|
156
|
+
createdAt: Date.now(),
|
|
157
|
+
}), { flag: "wx", mode: 0o600 });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code !== "EEXIST") throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
101
163
|
function markTarget(input, target, output) {
|
|
102
164
|
if (typeof target !== "string" || typeof output !== "string" || !output) return;
|
|
103
165
|
|
|
@@ -152,7 +214,88 @@ function markWait(input) {
|
|
|
152
214
|
}
|
|
153
215
|
|
|
154
216
|
function markSubagentStop(input) {
|
|
155
|
-
|
|
217
|
+
const state = typeof input.agent_id === "string" ? statePath(input, input.agent_id) : "";
|
|
218
|
+
if (state && existsSync(state)) {
|
|
219
|
+
markTarget(input, input.agent_id, input.last_assistant_message);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
persistPendingPipeline(input);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function consumePending(input) {
|
|
226
|
+
if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) return;
|
|
227
|
+
let root;
|
|
228
|
+
try {
|
|
229
|
+
root = realpathSync(process.cwd());
|
|
230
|
+
} catch {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
234
|
+
if (git.status !== 0 || realpathSync(git.stdout.trim()) !== root) return;
|
|
235
|
+
|
|
236
|
+
const id = checkoutId(root);
|
|
237
|
+
const hash = stateHash(root);
|
|
238
|
+
if (!hash) return;
|
|
239
|
+
const prefix = `${id}-${hash}-`;
|
|
240
|
+
const pendingPaths = existsSync(pendingDir())
|
|
241
|
+
? readdirSync(pendingDir())
|
|
242
|
+
.filter((name) => name.startsWith(prefix) && !name.endsWith(".claim") && !name.endsWith(".done"))
|
|
243
|
+
.map((name) => join(pendingDir(), name))
|
|
244
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
245
|
+
: [];
|
|
246
|
+
const path = pendingPaths[0];
|
|
247
|
+
if (!path) return;
|
|
248
|
+
|
|
249
|
+
const claim = `${path}.claim`;
|
|
250
|
+
try {
|
|
251
|
+
writeFileSync(claim, "", { flag: "wx", mode: 0o600 });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error?.code === "EEXIST") return;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const pending = JSON.parse(readFileSync(path, "utf8"));
|
|
259
|
+
const ttl = Number.parseInt(process.env.RISK_TTL || "3600", 10) * 1000;
|
|
260
|
+
if (pending.role !== "wr-risk-scorer:pipeline" || pending.checkoutId !== id ||
|
|
261
|
+
pending.stateHash !== hash || typeof pending.output !== "string" ||
|
|
262
|
+
typeof pending.completionId !== "string" || !path.endsWith(`-${pending.completionId}`) ||
|
|
263
|
+
!Number.isFinite(pending.createdAt) || Date.now() - pending.createdAt < 0 ||
|
|
264
|
+
Date.now() - pending.createdAt >= ttl) return;
|
|
265
|
+
|
|
266
|
+
const synthetic = {
|
|
267
|
+
...input,
|
|
268
|
+
cwd: root,
|
|
269
|
+
tool_name: "Agent",
|
|
270
|
+
tool_input: { subagent_type: pending.role, prompt: "" },
|
|
271
|
+
tool_response: { content: [{ type: "text", text: pending.output }] },
|
|
272
|
+
};
|
|
273
|
+
const result = spawnSync(join(hookDir, "risk-score-mark.sh"), {
|
|
274
|
+
cwd: root,
|
|
275
|
+
env: process.env,
|
|
276
|
+
input: JSON.stringify(synthetic),
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
});
|
|
279
|
+
if (result.status !== 0) {
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const assessedAt = new Date(pending.createdAt);
|
|
284
|
+
const markers = ["commit", "push", "release", "commit-born", "push-born", "release-born"];
|
|
285
|
+
if (/^RISK_BYPASS:\s*reducing\s*$/m.test(pending.output)) {
|
|
286
|
+
markers.push("reducing-commit", "reducing-push", "reducing-release");
|
|
287
|
+
} else if (/^RISK_BYPASS:\s*incident\s*$/m.test(pending.output)) {
|
|
288
|
+
markers.push("incident-release");
|
|
289
|
+
}
|
|
290
|
+
for (const marker of markers) {
|
|
291
|
+
const markerPath = join(riskDir(input.session_id), marker);
|
|
292
|
+
if (existsSync(markerPath)) utimesSync(markerPath, assessedAt, assessedAt);
|
|
293
|
+
}
|
|
294
|
+
renameSync(path, `${path}.done`);
|
|
295
|
+
for (const stale of pendingPaths.slice(1)) rmSync(stale, { force: true });
|
|
296
|
+
} finally {
|
|
297
|
+
rmSync(claim, { force: true });
|
|
298
|
+
}
|
|
156
299
|
}
|
|
157
300
|
|
|
158
301
|
let body = "";
|
|
@@ -166,6 +309,11 @@ try {
|
|
|
166
309
|
process.exit(0);
|
|
167
310
|
}
|
|
168
311
|
|
|
312
|
+
if (process.argv.includes("--consume-pending")) {
|
|
313
|
+
consumePending(input);
|
|
314
|
+
process.exit(process.exitCode || 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
169
317
|
if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) process.exit(0);
|
|
170
318
|
|
|
171
319
|
if (["collaborationspawn_agent", "spawn_agent", "multi_agent_v1__spawn_agent"].includes(input.tool_name)) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Consume a checkout-bound Codex SubagentStop receipt in the parent session.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/gate-helpers.sh"
|
|
8
|
+
_parse_input
|
|
9
|
+
|
|
10
|
+
TOOL_NAME="$(_get_tool_name)"
|
|
11
|
+
EVENT_NAME="$(printf '%s' "$_HOOK_INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("hook_event_name", ""))' 2>/dev/null || true)"
|
|
12
|
+
[ "$TOOL_NAME" = "Bash" ] || [ "$EVENT_NAME" = "UserPromptSubmit" ] || exit 0
|
|
13
|
+
_enter_hook_cwd || exit 0
|
|
14
|
+
printf '%s' "$_HOOK_INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs" --consume-pending
|
|
@@ -47,6 +47,9 @@ if messages:
|
|
|
47
47
|
fi
|
|
48
48
|
;;
|
|
49
49
|
user-prompt)
|
|
50
|
+
# Codex code-mode tools do not expose a parent PreToolUse event. Import a
|
|
51
|
+
# completed child receipt on the next already-enabled parent prompt.
|
|
52
|
+
"$SCRIPT_DIR/risk-pending-receipt.sh" <<<"$INPUT" || true
|
|
50
53
|
run_hook risk-score.sh
|
|
51
54
|
run_hook staleness-check.sh
|
|
52
55
|
;;
|
|
@@ -59,6 +62,10 @@ if messages:
|
|
|
59
62
|
run_hook risk-policy-enforce-edit.sh
|
|
60
63
|
;;
|
|
61
64
|
Bash)
|
|
65
|
+
# Current Codex emits SubagentStop in the child conversation but does
|
|
66
|
+
# not expose native collaboration calls to the parent's PostToolUse.
|
|
67
|
+
# Import the exact checkout/hash-bound receipt before enforcing gates.
|
|
68
|
+
"$SCRIPT_DIR/risk-pending-receipt.sh" <<<"$INPUT" || true
|
|
62
69
|
run_hook git-push-gate.sh
|
|
63
70
|
run_hook risk-score-commit-gate.sh
|
|
64
71
|
run_hook external-comms-gate.sh
|
package/package.json
CHANGED