@windyroad/risk-scorer 0.19.8-preview.1264 → 0.19.8
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.
|
@@ -25,6 +25,66 @@ function pendingDir() {
|
|
|
25
25
|
return join(process.env.TMPDIR || "/tmp", "claude-risk-pending");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function transportDir() {
|
|
29
|
+
return join(process.env.TMPDIR || "/tmp", "codex-review-transport");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeTarget(target) {
|
|
33
|
+
if (typeof target !== "string") return "";
|
|
34
|
+
return target.startsWith("/root/") ? target.slice("/root/".length) : target;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transportId(sessionId, role, target) {
|
|
38
|
+
return createHash("sha256").update(`${sessionId}\0${role}\0${target}`).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function transportRegistrationPath(sessionId, role, target) {
|
|
42
|
+
return join(transportDir(), `registration-${transportId(sessionId, role, target)}.json`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function transportReceiptPath(sessionId, role, target, suffix = "") {
|
|
46
|
+
return join(transportDir(), `risk-receipt-${transportId(sessionId, role, target)}.json${suffix}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function directoryBinding(cwd) {
|
|
50
|
+
if (typeof cwd !== "string" || !cwd) return null;
|
|
51
|
+
let root;
|
|
52
|
+
try { root = realpathSync(cwd); } catch { return null; }
|
|
53
|
+
const stat = statSync(root);
|
|
54
|
+
return { root, physical: `${stat.dev}:${stat.ino}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function policyHash(root) {
|
|
58
|
+
if (!existsSync(join(root, "RISK-POLICY.md"))) return null;
|
|
59
|
+
const result = spawnSync("bash", ["-c", 'source "$1"; _substance_hash_path RISK-POLICY.md', "risk-policy",
|
|
60
|
+
join(hookDir, "lib", "gate-helpers.sh")], { cwd: root, encoding: "utf8" });
|
|
61
|
+
const hash = result.stdout?.trim();
|
|
62
|
+
return result.status === 0 && /^[a-f0-9]{64}$/.test(hash || "") ? hash : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function policyBinding(root) {
|
|
66
|
+
const path = join(root, "RISK-POLICY.md");
|
|
67
|
+
if (!existsSync(path)) return { kind: "absent" };
|
|
68
|
+
const hash = policyHash(root);
|
|
69
|
+
return hash ? { kind: "hash", value: hash } : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function policyMatches(root, binding) {
|
|
73
|
+
if (!binding || !["absent", "hash"].includes(binding.kind)) return false;
|
|
74
|
+
const exists = existsSync(join(root, "RISK-POLICY.md"));
|
|
75
|
+
if (binding.kind === "absent") return !exists;
|
|
76
|
+
const current = policyHash(root);
|
|
77
|
+
return Boolean(current && current === binding.value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function expectedExternalKey(prompt) {
|
|
81
|
+
if (typeof prompt !== "string" || !prompt) return null;
|
|
82
|
+
const result = spawnSync("bash", ["-c", 'source "$1"; value="$(cat)"; derive_external_comms_key_from_prompt "$value"',
|
|
83
|
+
"external-key", join(hookDir, "lib", "external-comms-key.sh")], { input: prompt, encoding: "utf8" });
|
|
84
|
+
const key = result.stdout?.trim();
|
|
85
|
+
return result.status === 0 && /^[a-f0-9]{64}$/.test(key || "") ? key : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
function fieldType(input, field) {
|
|
29
89
|
if (!input || !Object.prototype.hasOwnProperty.call(input, field)) return "absent";
|
|
30
90
|
if (input[field] === null) return "null";
|
|
@@ -62,6 +122,18 @@ function statePath(input, target, suffix = "") {
|
|
|
62
122
|
}
|
|
63
123
|
|
|
64
124
|
function clearTarget(input, target) {
|
|
125
|
+
const state = statePath(input, target);
|
|
126
|
+
if (existsSync(state)) {
|
|
127
|
+
try {
|
|
128
|
+
const previousRole = readFileSync(state, "utf8");
|
|
129
|
+
if (riskAgentRoles.has(previousRole)) {
|
|
130
|
+
rmSync(transportRegistrationPath(input.session_id, previousRole, normalizeTarget(target)), { force: true });
|
|
131
|
+
for (const suffix of ["", ".claim", ".done"]) {
|
|
132
|
+
rmSync(transportReceiptPath(input.session_id, previousRole, normalizeTarget(target), suffix), { force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch { /* The local state is best-effort cleanup only. */ }
|
|
136
|
+
}
|
|
65
137
|
for (const suffix of ["", ".claim", ".done"]) {
|
|
66
138
|
rmSync(statePath(input, target, suffix), { force: true });
|
|
67
139
|
}
|
|
@@ -73,6 +145,7 @@ function spawnTarget(input) {
|
|
|
73
145
|
}
|
|
74
146
|
|
|
75
147
|
function rememberSpawn(input) {
|
|
148
|
+
reapRegisteredTransport();
|
|
76
149
|
const role = input.tool_input?.agent_type;
|
|
77
150
|
const target = spawnTarget(input);
|
|
78
151
|
if (typeof target !== "string" || !target) return;
|
|
@@ -80,6 +153,29 @@ function rememberSpawn(input) {
|
|
|
80
153
|
clearTarget(input, target);
|
|
81
154
|
if (!riskAgentRoles.has(role)) return;
|
|
82
155
|
writeFileSync(statePath(input, target), role, "utf8");
|
|
156
|
+
const normalized = normalizeTarget(target);
|
|
157
|
+
const bound = directoryBinding(input.cwd || process.cwd());
|
|
158
|
+
if (!bound) return;
|
|
159
|
+
const prompt = input.tool_input?.message ?? input.tool_input?.prompt ?? "";
|
|
160
|
+
const policy = policyBinding(bound.root);
|
|
161
|
+
if (!policy) return;
|
|
162
|
+
const wipStateHash = role === "wr-risk-scorer:wip" ? stateHash(bound.root) : null;
|
|
163
|
+
const wipCheckoutId = role === "wr-risk-scorer:wip" ? checkoutId(bound.root) : null;
|
|
164
|
+
if (role === "wr-risk-scorer:wip" && (!wipStateHash || !wipCheckoutId)) return;
|
|
165
|
+
const registered = {
|
|
166
|
+
parentSession: input.session_id,
|
|
167
|
+
role,
|
|
168
|
+
target: normalized,
|
|
169
|
+
...bound,
|
|
170
|
+
policy,
|
|
171
|
+
promptDigest: createHash("sha256").update(prompt).digest("hex"),
|
|
172
|
+
expectedKey: role === "wr-risk-scorer:external-comms" ? expectedExternalKey(prompt) : null,
|
|
173
|
+
wipStateHash,
|
|
174
|
+
wipCheckoutId,
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
mkdirSync(transportDir(), { recursive: true, mode: 0o700 });
|
|
178
|
+
writeFileSync(transportRegistrationPath(input.session_id, role, normalized), JSON.stringify(registered), { mode: 0o600 });
|
|
83
179
|
}
|
|
84
180
|
|
|
85
181
|
function claimTarget(input, target) {
|
|
@@ -159,92 +255,6 @@ function stateHash(root) {
|
|
|
159
255
|
return createHash("md5").update(state.stdout).digest("hex");
|
|
160
256
|
}
|
|
161
257
|
|
|
162
|
-
function completionId(input, output) {
|
|
163
|
-
if (typeof input.session_id !== "string" || typeof input.agent_id !== "string") return null;
|
|
164
|
-
return createHash("sha256").update(`${input.session_id}\0${input.agent_id}\0${output}`).digest("hex");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function pendingPath(id, hash, completion, suffix = "") {
|
|
168
|
-
return join(pendingDir(), `${id}-${hash}-${completion}${suffix}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function freshReceipt(path) {
|
|
172
|
-
if (!existsSync(path)) return false;
|
|
173
|
-
const ttl = Number.parseInt(process.env.RISK_TTL || "3600", 10) * 1000;
|
|
174
|
-
return Date.now() - statSync(path).mtimeMs < ttl;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function reapExpiredReceipts() {
|
|
178
|
-
if (!existsSync(pendingDir())) return;
|
|
179
|
-
const receipt = /^[0-9a-f]{64}-[0-9a-f]{32}-[0-9a-f]{64}(?:\.claim|\.done)?$/;
|
|
180
|
-
for (const name of readdirSync(pendingDir())) {
|
|
181
|
-
if (!receipt.test(name)) continue;
|
|
182
|
-
const path = join(pendingDir(), name);
|
|
183
|
-
try {
|
|
184
|
-
if (!freshReceipt(path)) rmSync(path, { force: true });
|
|
185
|
-
} catch {
|
|
186
|
-
// A concurrent consumer may have renamed or removed it.
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function persistPendingPipeline(input) {
|
|
192
|
-
reapExpiredReceipts();
|
|
193
|
-
diagnoseSubagentStop(input, "received", "pipeline-receipt-attempt");
|
|
194
|
-
if (input.agent_type !== "wr-risk-scorer:pipeline") {
|
|
195
|
-
diagnoseSubagentStop(input, "rejected", "unexpected-agent-type");
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
let rejection;
|
|
199
|
-
const assessment = pipelineAssessment(input.last_assistant_message, (reason) => { rejection = reason; });
|
|
200
|
-
if (!assessment) {
|
|
201
|
-
diagnoseSubagentStop(input, "rejected", rejection || "invalid-assessment");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const id = checkoutId(assessment.root);
|
|
205
|
-
const hash = stateHash(assessment.root);
|
|
206
|
-
const completion = completionId(input, assessment.output);
|
|
207
|
-
if (!hash) {
|
|
208
|
-
diagnoseSubagentStop(input, "rejected", "state-hash-failed");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!completion) {
|
|
212
|
-
diagnoseSubagentStop(input, "rejected", "missing-completion-identity");
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (!/^RISK_SCORES: commit=\d+ push=\d+ release=\d+$/m.test(assessment.output)) {
|
|
216
|
-
diagnoseSubagentStop(input, "rejected", "missing-risk-scores");
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
mkdirSync(pendingDir(), { recursive: true });
|
|
220
|
-
const path = pendingPath(id, hash, completion);
|
|
221
|
-
for (const candidate of [path, `${path}.done`]) {
|
|
222
|
-
if (freshReceipt(candidate)) {
|
|
223
|
-
diagnoseSubagentStop(input, "duplicate", "fresh-receipt-exists");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
rmSync(candidate, { force: true });
|
|
227
|
-
}
|
|
228
|
-
try {
|
|
229
|
-
writeFileSync(path, JSON.stringify({
|
|
230
|
-
role: input.agent_type,
|
|
231
|
-
output: assessment.output,
|
|
232
|
-
checkoutId: id,
|
|
233
|
-
stateHash: hash,
|
|
234
|
-
completionId: completion,
|
|
235
|
-
createdAt: Date.now(),
|
|
236
|
-
}), { flag: "wx", mode: 0o600 });
|
|
237
|
-
diagnoseSubagentStop(input, "receipt-written", "checkout-bound-receipt");
|
|
238
|
-
} catch (error) {
|
|
239
|
-
if (error?.code === "EEXIST") {
|
|
240
|
-
diagnoseSubagentStop(input, "duplicate", "receipt-race");
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
diagnoseSubagentStop(input, "rejected", "receipt-write-failed");
|
|
244
|
-
throw error;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
258
|
function markTarget(input, target, output) {
|
|
249
259
|
if (typeof target !== "string" || typeof output !== "string" || !output) return;
|
|
250
260
|
|
|
@@ -280,6 +290,7 @@ function markTarget(input, target, output) {
|
|
|
280
290
|
if (result.status === 0) {
|
|
281
291
|
renameSync(claim.claim, claim.done);
|
|
282
292
|
rmSync(state, { force: true });
|
|
293
|
+
rmSync(transportRegistrationPath(input.session_id, role, normalizeTarget(target)), { force: true });
|
|
283
294
|
} else {
|
|
284
295
|
rmSync(claim.claim, { force: true });
|
|
285
296
|
process.exitCode = 1;
|
|
@@ -298,90 +309,199 @@ function markWait(input) {
|
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
|
|
301
|
-
function
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
312
|
+
function genericTtl() {
|
|
313
|
+
const text = process.env.RISK_TTL || "3600";
|
|
314
|
+
if (!/^[0-9]+$/.test(text)) return null;
|
|
315
|
+
const value = Number(text) * 1000;
|
|
316
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function reapRegisteredTransport() {
|
|
320
|
+
const ttl = genericTtl();
|
|
321
|
+
if (!ttl || !existsSync(transportDir())) return;
|
|
322
|
+
for (const name of readdirSync(transportDir())) {
|
|
323
|
+
const path = join(transportDir(), name);
|
|
324
|
+
try {
|
|
325
|
+
const statAge = Date.now() - statSync(path).mtimeMs;
|
|
326
|
+
if (/^(?:receipt|risk-receipt)-[a-f0-9]{64}\.json\.(?:claim|done)$/.test(name)) {
|
|
327
|
+
if (!Number.isFinite(statAge) || statAge < 0 || statAge >= ttl) rmSync(path, { force: true });
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!/^(?:registration|risk-receipt)-[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
331
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
332
|
+
if (!riskAgentRoles.has(value.role)) continue;
|
|
333
|
+
const timestamp = name.startsWith("risk-receipt-") ? value.completedAt : value.createdAt;
|
|
334
|
+
const age = Date.now() - timestamp;
|
|
335
|
+
if (!Number.isFinite(age) || age < 0 || age >= ttl) rmSync(path, { force: true });
|
|
336
|
+
} catch { /* Another completion may own or remove this transport file. */ }
|
|
306
337
|
}
|
|
307
|
-
persistPendingPipeline(input);
|
|
308
338
|
}
|
|
309
339
|
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
340
|
+
function registrationCandidates(input, role, target) {
|
|
341
|
+
reapRegisteredTransport();
|
|
342
|
+
if (!existsSync(transportDir())) return [];
|
|
343
|
+
const current = directoryBinding(input.cwd || process.cwd());
|
|
344
|
+
return readdirSync(transportDir())
|
|
345
|
+
.filter((name) => /^registration-[a-f0-9]{64}\.json$/.test(name))
|
|
346
|
+
.flatMap((name) => {
|
|
347
|
+
const path = join(transportDir(), name);
|
|
348
|
+
try {
|
|
349
|
+
const registered = JSON.parse(readFileSync(path, "utf8"));
|
|
350
|
+
if (registered.role !== role || registered.target !== target) return [];
|
|
351
|
+
if (role !== "wr-risk-scorer:pipeline" && (!current || registered.root !== current.root || registered.physical !== current.physical)) return [];
|
|
352
|
+
return [{ path, registered }];
|
|
353
|
+
} catch { return []; }
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function singleLine(output, label) {
|
|
358
|
+
return [...output.matchAll(new RegExp(`^${label}:[ \\t]*(.+)$`, "gm"))];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function applicableAssessment(role, output, registered, reject = () => {}) {
|
|
362
|
+
if (typeof output !== "string" || !output) return reject("missing-output"), null;
|
|
363
|
+
if (role === "wr-risk-scorer:pipeline") {
|
|
364
|
+
let reason;
|
|
365
|
+
const assessment = pipelineAssessment(output, (value) => { reason = value; });
|
|
366
|
+
if (!assessment || !/^RISK_SCORES: commit=\d+ push=\d+ release=\d+$/m.test(assessment.output)) {
|
|
367
|
+
reject(reason || "missing-risk-scores");
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const hash = stateHash(assessment.root);
|
|
371
|
+
if (!hash) return reject("state-hash-failed"), null;
|
|
372
|
+
const assessmentPolicy = policyBinding(assessment.root);
|
|
373
|
+
if (!assessmentPolicy) return reject("policy-hash-failed"), null;
|
|
374
|
+
return { output: assessment.output, assessmentRoot: assessment.root, assessmentPolicy,
|
|
375
|
+
checkoutId: checkoutId(assessment.root), stateHash: hash };
|
|
376
|
+
}
|
|
377
|
+
if (role === "wr-risk-scorer:external-comms") {
|
|
378
|
+
const verdicts = singleLine(output, "EXTERNAL_COMMS_RISK_VERDICT");
|
|
379
|
+
const keys = singleLine(output, "EXTERNAL_COMMS_RISK_KEY");
|
|
380
|
+
const key = keys.length === 1 ? keys[0][1].trim().replaceAll("`", "") : "";
|
|
381
|
+
if (verdicts.length !== 1 || verdicts[0][1].trim() !== "PASS") return reject("non-pass-verdict"), null;
|
|
382
|
+
if (!/^[a-f0-9]{64}$/.test(key)) return reject("invalid-external-comms-key"), null;
|
|
383
|
+
if (!registered.expectedKey) return reject("missing-external-comms-key-binding"), null;
|
|
384
|
+
if (registered.expectedKey !== key) return reject("external-comms-key-mismatch"), null;
|
|
385
|
+
return { output, root: registered.root, key };
|
|
386
|
+
}
|
|
387
|
+
if (role === "wr-risk-scorer:plan" || role === "wr-risk-scorer:policy") {
|
|
388
|
+
const verdicts = singleLine(output, "RISK_VERDICT");
|
|
389
|
+
if (verdicts.length !== 1 || verdicts[0][1].trim() !== "PASS") return reject("non-pass-verdict"), null;
|
|
390
|
+
return { output, root: registered.root };
|
|
391
|
+
}
|
|
392
|
+
if (role === "wr-risk-scorer:wip") {
|
|
393
|
+
const verdicts = singleLine(output, "RISK_VERDICT");
|
|
394
|
+
if (verdicts.length !== 1 || !/^(CONTINUE|COMMIT)$/.test(verdicts[0][1].trim())) return reject("non-authorizing-wip-verdict"), null;
|
|
395
|
+
const currentHash = stateHash(registered.root);
|
|
396
|
+
if (!currentHash || currentHash !== registered.wipStateHash || checkoutId(registered.root) !== registered.wipCheckoutId) return reject("wip-state-changed"), null;
|
|
397
|
+
return { output, root: registered.root, wipStateHash: currentHash, wipCheckoutId: registered.wipCheckoutId };
|
|
398
|
+
}
|
|
399
|
+
reject("unsupported-marker-role");
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function registrationValid(input, registered) {
|
|
404
|
+
const ttl = genericTtl();
|
|
405
|
+
const age = Date.now() - registered.createdAt;
|
|
406
|
+
if (!ttl) return diagnoseSubagentStop(input, "rejected", "invalid-risk-ttl"), false;
|
|
407
|
+
if (!Number.isFinite(age) || age < 0 || age >= ttl) return diagnoseSubagentStop(input, "rejected", "stale-parent-registration"), false;
|
|
408
|
+
if (!registered.policy || !["absent", "hash"].includes(registered.policy.kind)) return diagnoseSubagentStop(input, "rejected", "invalid-policy-binding"), false;
|
|
409
|
+
if (!policyMatches(registered.root, registered.policy)) return diagnoseSubagentStop(input, "rejected", "policy-changed"), false;
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function persistRegisteredPending(input) {
|
|
414
|
+
const role = input.agent_type;
|
|
415
|
+
const target = normalizeTarget(input.task_name || input.agent_name || input.agent_id);
|
|
416
|
+
if (!riskAgentRoles.has(role) || !target) return false;
|
|
417
|
+
const candidates = registrationCandidates(input, role, target);
|
|
418
|
+
if (candidates.length !== 1) {
|
|
419
|
+
diagnoseSubagentStop(input, "rejected", candidates.length ? "ambiguous-parent-registration" : "missing-parent-registration");
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const { registered } = candidates[0];
|
|
423
|
+
if (!registrationValid(input, registered)) return true;
|
|
424
|
+
let rejection;
|
|
425
|
+
const assessment = applicableAssessment(role, input.last_assistant_message, registered, (reason) => { rejection = reason; });
|
|
426
|
+
if (!assessment) {
|
|
427
|
+
diagnoseSubagentStop(input, "rejected", rejection || "invalid-assessment");
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
const path = transportReceiptPath(registered.parentSession, role, target);
|
|
431
|
+
if (existsSync(path) || existsSync(`${path}.done`)) {
|
|
432
|
+
diagnoseSubagentStop(input, "duplicate", "fresh-parent-bound-receipt-exists");
|
|
433
|
+
return true;
|
|
318
434
|
}
|
|
319
|
-
const git = spawnSync("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
320
|
-
if (git.status !== 0 || realpathSync(git.stdout.trim()) !== root) return;
|
|
321
|
-
|
|
322
|
-
const id = checkoutId(root);
|
|
323
|
-
const hash = stateHash(root);
|
|
324
|
-
if (!hash) return;
|
|
325
|
-
const prefix = `${id}-${hash}-`;
|
|
326
|
-
const pendingPaths = existsSync(pendingDir())
|
|
327
|
-
? readdirSync(pendingDir())
|
|
328
|
-
.filter((name) => name.startsWith(prefix) && !name.endsWith(".claim") && !name.endsWith(".done"))
|
|
329
|
-
.map((name) => join(pendingDir(), name))
|
|
330
|
-
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
|
|
331
|
-
: [];
|
|
332
|
-
const path = pendingPaths[0];
|
|
333
|
-
if (!path) return;
|
|
334
|
-
|
|
335
|
-
const claim = `${path}.claim`;
|
|
336
435
|
try {
|
|
337
|
-
writeFileSync(
|
|
436
|
+
writeFileSync(path, JSON.stringify({ ...registered, ...assessment, completedAt: Date.now() }), { flag: "wx", mode: 0o600 });
|
|
437
|
+
diagnoseSubagentStop(input, "receipt-written", "parent-bound-receipt");
|
|
338
438
|
} catch (error) {
|
|
339
|
-
|
|
340
|
-
throw error;
|
|
439
|
+
diagnoseSubagentStop(input, error?.code === "EEXIST" ? "duplicate" : "rejected", error?.code === "EEXIST" ? "receipt-race" : "receipt-write-failed");
|
|
341
440
|
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
342
443
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const markers = ["commit", "push", "release", "commit-born", "push-born", "release-born"];
|
|
371
|
-
if (/^RISK_BYPASS:\s*reducing\s*$/m.test(pending.output)) {
|
|
372
|
-
markers.push("reducing-commit", "reducing-push", "reducing-release");
|
|
373
|
-
} else if (/^RISK_BYPASS:\s*incident\s*$/m.test(pending.output)) {
|
|
374
|
-
markers.push("incident-release");
|
|
375
|
-
}
|
|
376
|
-
for (const marker of markers) {
|
|
377
|
-
const markerPath = join(riskDir(input.session_id), marker);
|
|
378
|
-
if (existsSync(markerPath)) utimesSync(markerPath, assessedAt, assessedAt);
|
|
444
|
+
function markerPaths(role, session, pending) {
|
|
445
|
+
const dir = riskDir(session);
|
|
446
|
+
if (role === "wr-risk-scorer:pipeline") return ["commit", "push", "release", "commit-born", "push-born", "release-born", "state-hash", "checkout-id"].map((name) => join(dir, name));
|
|
447
|
+
if (role === "wr-risk-scorer:plan") return [join(dir, "plan-reviewed"), join(dir, "state-hash")];
|
|
448
|
+
if (role === "wr-risk-scorer:wip") return [join(dir, "wip-reviewed")];
|
|
449
|
+
if (role === "wr-risk-scorer:policy") return [join(dir, "policy-reviewed")];
|
|
450
|
+
if (role === "wr-risk-scorer:external-comms") return [join(dir, `external-comms-risk-reviewed-${pending.key}`)];
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function consumeRegisteredPending(input) {
|
|
455
|
+
reapRegisteredTransport();
|
|
456
|
+
if (!existsSync(transportDir())) return;
|
|
457
|
+
const current = directoryBinding(process.cwd());
|
|
458
|
+
for (const name of readdirSync(transportDir()).filter((entry) => /^risk-receipt-[a-f0-9]{64}\.json$/.test(entry))) {
|
|
459
|
+
const path = join(transportDir(), name);
|
|
460
|
+
let pending;
|
|
461
|
+
try { pending = JSON.parse(readFileSync(path, "utf8")); } catch { continue; }
|
|
462
|
+
if (pending.parentSession !== input.session_id || !riskAgentRoles.has(pending.role)) continue;
|
|
463
|
+
if (!registrationValid(input, pending)) continue;
|
|
464
|
+
if (pending.role === "wr-risk-scorer:pipeline") {
|
|
465
|
+
if (!current || checkoutId(current.root) !== pending.checkoutId || stateHash(current.root) !== pending.stateHash) continue;
|
|
466
|
+
if (current.root !== pending.assessmentRoot || !policyMatches(current.root, pending.assessmentPolicy)) continue;
|
|
467
|
+
} else {
|
|
468
|
+
if (!current || current.root !== pending.root || current.physical !== pending.physical) continue;
|
|
469
|
+
if (pending.role === "wr-risk-scorer:wip" &&
|
|
470
|
+
(checkoutId(current.root) !== pending.wipCheckoutId || stateHash(current.root) !== pending.wipStateHash)) continue;
|
|
379
471
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
472
|
+
const claim = `${path}.claim`;
|
|
473
|
+
try { writeFileSync(claim, "", { flag: "wx", mode: 0o600 }); }
|
|
474
|
+
catch (error) { if (error?.code === "EEXIST") continue; throw error; }
|
|
475
|
+
try {
|
|
476
|
+
const markerRoot = pending.role === "wr-risk-scorer:pipeline" ? pending.assessmentRoot : pending.root;
|
|
477
|
+
const synthetic = { ...input, session_id: pending.parentSession, cwd: markerRoot, tool_name: "Agent",
|
|
478
|
+
tool_input: { subagent_type: pending.role, prompt: "" }, tool_response: { content: [{ type: "text", text: pending.output }] } };
|
|
479
|
+
const result = spawnSync(join(hookDir, "risk-score-mark.sh"), { cwd: markerRoot, env: process.env, input: JSON.stringify(synthetic), encoding: "utf8" });
|
|
480
|
+
if (result.status !== 0) {
|
|
481
|
+
diagnoseSubagentStop(input, "rejected", "marker-writer-failed");
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const assessedAt = new Date(pending.completedAt);
|
|
485
|
+
for (const marker of markerPaths(pending.role, pending.parentSession, pending)) if (existsSync(marker)) utimesSync(marker, assessedAt, assessedAt);
|
|
486
|
+
renameSync(path, `${path}.done`);
|
|
487
|
+
rmSync(transportRegistrationPath(pending.parentSession, pending.role, pending.target), { force: true });
|
|
488
|
+
rmSync(statePath({ session_id: pending.parentSession }, pending.target), { force: true });
|
|
489
|
+
} finally { rmSync(claim, { force: true }); }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function markSubagentStop(input) {
|
|
494
|
+
const state = typeof input.agent_id === "string" ? statePath(input, input.agent_id) : "";
|
|
495
|
+
if (state && existsSync(state)) {
|
|
496
|
+
markTarget(input, input.agent_id, input.last_assistant_message);
|
|
497
|
+
return;
|
|
384
498
|
}
|
|
499
|
+
persistRegisteredPending(input);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function consumePending(input) {
|
|
503
|
+
if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) return;
|
|
504
|
+
consumeRegisteredPending(input);
|
|
385
505
|
}
|
|
386
506
|
|
|
387
507
|
let body = "";
|
|
@@ -398,27 +518,18 @@ try {
|
|
|
398
518
|
process.exit(0);
|
|
399
519
|
}
|
|
400
520
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
521
|
+
try {
|
|
522
|
+
if (process.argv.includes("--consume-pending")) {
|
|
523
|
+
consumePending(input);
|
|
524
|
+
} else if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) {
|
|
525
|
+
if (process.argv.includes("--subagent-stop")) diagnoseSubagentStop(input, "rejected", "invalid-session-id");
|
|
526
|
+
} else {
|
|
527
|
+
if (SPAWN_TOOLS.has(input.tool_name)) rememberSpawn(input);
|
|
528
|
+
if (CLOSE_TOOLS.has(input.tool_name)) markClose(input);
|
|
529
|
+
if (WAIT_TOOLS.has(input.tool_name)) markWait(input);
|
|
530
|
+
if (input.hook_event_name === "SubagentStop") markSubagentStop(input);
|
|
409
531
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (SPAWN_TOOLS.has(input.tool_name)) {
|
|
414
|
-
rememberSpawn(input);
|
|
415
|
-
}
|
|
416
|
-
if (CLOSE_TOOLS.has(input.tool_name)) {
|
|
417
|
-
markClose(input);
|
|
418
|
-
}
|
|
419
|
-
if (WAIT_TOOLS.has(input.tool_name)) {
|
|
420
|
-
markWait(input);
|
|
421
|
-
}
|
|
422
|
-
if (input.hook_event_name === "SubagentStop") {
|
|
423
|
-
markSubagentStop(input);
|
|
532
|
+
} catch {
|
|
533
|
+
diagnoseSubagentStop(input, "rejected", "transport-error");
|
|
534
|
+
process.exitCode = 0;
|
|
424
535
|
}
|
|
@@ -9,6 +9,6 @@ _parse_input
|
|
|
9
9
|
|
|
10
10
|
TOOL_NAME="$(_get_tool_name)"
|
|
11
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
|
-
[ "$
|
|
12
|
+
[ "$EVENT_NAME" = "PreToolUse" ] || [ "$EVENT_NAME" = "UserPromptSubmit" ] || [ "$TOOL_NAME" = "Bash" ] || exit 0
|
|
13
13
|
_enter_hook_cwd || exit 0
|
|
14
14
|
printf '%s' "$_HOOK_INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs" --consume-pending
|
|
@@ -55,6 +55,8 @@ if messages:
|
|
|
55
55
|
run_hook staleness-check.sh
|
|
56
56
|
;;
|
|
57
57
|
pre-tool)
|
|
58
|
+
# Import an exact-parent background receipt before every guarded action.
|
|
59
|
+
"$SCRIPT_DIR/risk-pending-receipt.sh" <<<"$INPUT" || true
|
|
58
60
|
case "$(tool_name)" in
|
|
59
61
|
Edit|Write)
|
|
60
62
|
run_hook secret-leak-gate.sh
|
|
@@ -63,10 +65,6 @@ if messages:
|
|
|
63
65
|
run_hook risk-policy-enforce-edit.sh
|
|
64
66
|
;;
|
|
65
67
|
Bash)
|
|
66
|
-
# Current Codex emits SubagentStop in the child conversation but does
|
|
67
|
-
# not expose native collaboration calls to the parent's PostToolUse.
|
|
68
|
-
# Import the exact checkout/hash-bound receipt before enforcing gates.
|
|
69
|
-
"$SCRIPT_DIR/risk-pending-receipt.sh" <<<"$INPUT" || true
|
|
70
68
|
run_hook git-push-gate.sh
|
|
71
69
|
run_hook risk-score-commit-gate.sh
|
|
72
70
|
run_hook external-comms-gate.sh
|
package/package.json
CHANGED