@windyroad/risk-scorer 0.19.15 → 0.19.16

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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.19.15"
313
+ "version": "0.19.16"
314
314
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-risk-scorer",
3
- "version": "0.19.15",
3
+ "version": "0.19.16",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
@@ -43,6 +43,11 @@ function transportRegistrationPath(sessionId, role, target) {
43
43
  return join(transportDir(), `registration-${transportId(sessionId, role, target)}.json`);
44
44
  }
45
45
 
46
+ function registeredPath(registered) {
47
+ const target = registered.source === "subagent-start" ? `native:${registered.target}` : registered.target;
48
+ return transportRegistrationPath(registered.parentSession, registered.role, target);
49
+ }
50
+
46
51
  function transportReceiptPath(sessionId, role, target, suffix = "") {
47
52
  return join(transportDir(), `risk-receipt-${transportId(sessionId, role, target)}.json${suffix}`);
48
53
  }
@@ -145,9 +150,49 @@ function spawnTarget(input) {
145
150
  return result.agent_id ?? result.task_name;
146
151
  }
147
152
 
153
+ function nativePipelinePath(sessionId) {
154
+ return join(riskDir(sessionId), "native-pipeline-transport");
155
+ }
156
+
157
+ // Native lifecycle events carry the runtime-owned parent session and agent ID.
158
+ // They do not depend on the spawn tool being visible to PostToolUse.
159
+ function rememberNativePipeline(input) {
160
+ if (input.agent_type !== "wr-risk-scorer:pipeline" ||
161
+ typeof input.agent_id !== "string" || !/^[A-Za-z0-9-]+$/.test(input.agent_id)) return;
162
+ const bound = directoryBinding(input.cwd);
163
+ if (!bound) return;
164
+ const policy = policyBinding(bound.root);
165
+ if (!policy) return;
166
+ const target = input.agent_id;
167
+ if (existsSync(statePath(input, target, ".done")) ||
168
+ existsSync(transportReceiptPath(input.session_id, input.agent_type, target, ".done"))) return;
169
+ const registered = {
170
+ source: "subagent-start", parentSession: input.session_id,
171
+ role: input.agent_type, target, ...bound, policy, createdAt: Date.now(),
172
+ };
173
+ mkdirSync(transportDir(), { recursive: true, mode: 0o700 });
174
+ const path = registeredPath(registered);
175
+ try {
176
+ writeFileSync(path, JSON.stringify(registered), { flag: "wx", mode: 0o600 });
177
+ } catch (error) {
178
+ if (error?.code !== "EEXIST") throw error;
179
+ const previous = JSON.parse(readFileSync(path, "utf8"));
180
+ for (const key of ["source", "parentSession", "role", "target", "root", "physical"]) {
181
+ if (previous[key] !== registered[key]) return;
182
+ }
183
+ if (JSON.stringify(previous.policy) !== JSON.stringify(policy)) return;
184
+ }
185
+ // This records transport availability, not assessment authorization. Keep it
186
+ // after receipt cleanup so delayed legacy delivery cannot refresh scores.
187
+ mkdirSync(riskDir(input.session_id), { recursive: true });
188
+ try { writeFileSync(nativePipelinePath(input.session_id), "", { flag: "wx", mode: 0o600 }); }
189
+ catch (error) { if (error?.code !== "EEXIST") throw error; }
190
+ }
191
+
148
192
  function rememberSpawn(input) {
149
193
  reapRegisteredTransport();
150
194
  const role = input.tool_input?.agent_type;
195
+ if (role === "wr-risk-scorer:pipeline" && existsSync(nativePipelinePath(input.session_id))) return;
151
196
  const target = spawnTarget(input);
152
197
  if (typeof target !== "string" || !target) return;
153
198
  mkdirSync(riskDir(input.session_id), { recursive: true });
@@ -265,6 +310,7 @@ function markTarget(input, target, output) {
265
310
  if (!existsSync(state)) return;
266
311
  const role = readFileSync(state, "utf8");
267
312
  if (!riskAgentRoles.has(role)) return;
313
+ if (role === "wr-risk-scorer:pipeline" && existsSync(nativePipelinePath(input.session_id))) return;
268
314
  const claim = claimTarget(input, target);
269
315
  if (!claim) return;
270
316
 
@@ -352,16 +398,23 @@ function registrationCandidates(input, role, target) {
352
398
  try {
353
399
  const registered = JSON.parse(readFileSync(path, "utf8"));
354
400
  if (registered.role !== role) return [];
401
+ if (role === "wr-risk-scorer:pipeline" && existsSync(nativePipelinePath(input.session_id)) &&
402
+ registered.source !== "subagent-start") return [];
403
+ if (registered.source === "subagent-start" &&
404
+ (registered.parentSession !== input.session_id || registered.target !== input.agent_id)) return [];
355
405
  if (role !== "wr-risk-scorer:pipeline" && (!current || registered.root !== current.root || registered.physical !== current.physical)) return [];
356
406
  return [{ path, registered }];
357
407
  } catch { return []; }
358
408
  });
409
+ const native = registrations.filter(({ registered }) => registered.source === "subagent-start");
410
+ if (native.length) return native;
359
411
  const exact = registrations.filter(({ registered }) => registered.target === target);
360
412
  if (exact.length) return exact;
361
413
  // Current Codex returns a task name at spawn and a child UUID at stop.
362
414
  // The runtime parent session plus one invoking-checkout registration binds them.
363
415
  if (input.hook_event_name !== "SubagentStop" || !current) return [];
364
416
  return registrations.filter(({ registered }) =>
417
+ registered.source !== "subagent-start" &&
365
418
  registered.parentSession === input.session_id &&
366
419
  registered.root === current.root && registered.physical === current.physical);
367
420
  }
@@ -473,6 +526,7 @@ function consumeRegisteredPending(input) {
473
526
  let pending;
474
527
  try { pending = JSON.parse(readFileSync(path, "utf8")); } catch { continue; }
475
528
  if (pending.parentSession !== input.session_id || !riskAgentRoles.has(pending.role)) continue;
529
+ if (pending.role === "wr-risk-scorer:pipeline" && existsSync(nativePipelinePath(input.session_id)) && pending.source !== "subagent-start") continue;
476
530
  if (!registrationValid(input, pending)) continue;
477
531
  if (pending.role === "wr-risk-scorer:pipeline") {
478
532
  if (!current || checkoutId(current.root) !== pending.checkoutId || stateHash(current.root) !== pending.stateHash) continue;
@@ -492,7 +546,7 @@ function consumeRegisteredPending(input) {
492
546
  if (!targetClaim) {
493
547
  if (existsSync(statePath(targetInput, pending.target, ".done"))) {
494
548
  renameSync(path, `${path}.done`);
495
- rmSync(transportRegistrationPath(pending.parentSession, pending.role, pending.target), { force: true });
549
+ rmSync(registeredPath(pending), { force: true });
496
550
  }
497
551
  continue;
498
552
  }
@@ -508,7 +562,7 @@ function consumeRegisteredPending(input) {
508
562
  for (const marker of markerPaths(pending.role, pending.parentSession, pending)) if (existsSync(marker)) utimesSync(marker, assessedAt, assessedAt);
509
563
  renameSync(targetClaim.claim, targetClaim.done);
510
564
  renameSync(path, `${path}.done`);
511
- rmSync(transportRegistrationPath(pending.parentSession, pending.role, pending.target), { force: true });
565
+ rmSync(registeredPath(pending), { force: true });
512
566
  rmSync(statePath(targetInput, pending.target), { force: true });
513
567
  } finally {
514
568
  if (targetClaim) rmSync(targetClaim.claim, { force: true });
@@ -518,6 +572,10 @@ function consumeRegisteredPending(input) {
518
572
  }
519
573
 
520
574
  function markSubagentStop(input) {
575
+ if (input.agent_type === "wr-risk-scorer:pipeline" && existsSync(nativePipelinePath(input.session_id))) {
576
+ persistRegisteredPending(input);
577
+ return;
578
+ }
521
579
  const state = typeof input.agent_id === "string" ? statePath(input, input.agent_id) : "";
522
580
  if (state && existsSync(state)) {
523
581
  markTarget(input, input.agent_id, input.last_assistant_message);
@@ -551,6 +609,7 @@ try {
551
609
  } else if (!/^[A-Za-z0-9-]+$/.test(input.session_id || "")) {
552
610
  if (process.argv.includes("--subagent-stop")) diagnoseSubagentStop(input, "rejected", "invalid-session-id");
553
611
  } else {
612
+ if (input.hook_event_name === "SubagentStart") rememberNativePipeline(input);
554
613
  if (SPAWN_TOOLS.has(input.tool_name)) rememberSpawn(input);
555
614
  if (CLOSE_TOOLS.has(input.tool_name)) markClose(input);
556
615
  if (WAIT_TOOLS.has(input.tool_name)) markWait(input);
package/hooks/hooks.json CHANGED
@@ -12,6 +12,9 @@
12
12
  "PostToolUse": [
13
13
  { "matcher": "Agent|Bash|Edit|Skill|Write|collaboration\\.spawn_agent|collaboration\\.wait_agent|collaboration\\.interrupt_agent|collaborationspawn_agent|collaborationwait_agent|collaborationinterrupt_agent|spawn_agent|wait_agent|interrupt_agent|close_agent|multi_agent_v1__spawn_agent|multi_agent_v1__wait_agent|multi_agent_v1__close_agent", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh post-tool" }] }
14
14
  ],
15
+ "SubagentStart": [
16
+ { "matcher": "^wr-risk-scorer:pipeline$", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh subagent-start" }] }
17
+ ],
15
18
  "SubagentStop": [
16
19
  { "matcher": "^wr-risk-scorer:", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/risk-scorer-dispatch.sh subagent-stop" }] }
17
20
  ]
@@ -98,6 +98,9 @@ if messages:
98
98
  ;;
99
99
  esac
100
100
  ;;
101
+ subagent-start)
102
+ printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs" --subagent-start
103
+ ;;
101
104
  subagent-stop)
102
105
  printf '%s' "$INPUT" | node "$SCRIPT_DIR/codex-agent-completion.mjs" --subagent-stop
103
106
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.19.15",
3
+ "version": "0.19.16",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "scripts": {
6
6
  "prepack": "node scripts/sync-codex-skills.mjs --pack",