Read-Only Security Review · Remediation Record

Sentinel Review Dossier

BOB Sentinel, Agent Worker, and the webhook → dispatcher → worker → review-gate pipeline on Node1. An independent review of authorization, tenant isolation, approval handling, and emergency controls — followed by same-night remediation and live verification.

Reviewed system: BOB Sentinel + Identity v1 Scope: webhook-runtime · agent-dispatcher · agent-worker Date: 2026-09-24
9
Findings fixed & live-verified
3
Deferred, blocked on a decision
12
Total findings from review
0
Unauthorized events, ever
·

The BOB Ecosystem

This review covers one agent family: Scout — the shared identity behind Repository-Monitor-Agent, GitHub-Issue-Agent, and PR-Review-Status-Agent (see agentAliases in sentinel-runtime.json). The other four are shown for ecosystem context; none of them were part of tonight's review.

This reviewScout — finds answers
Ecosystem contextHoudini — learns by doing
Ecosystem contextPulse — keeps you well
Ecosystem contextOrbit — keeps everything aligned
Ecosystem contextRelay — connects people
01

Summary

The review examined the Sentinel authorization gate, the evidence-validated approval flow in Agent Worker, and the read-only webhook pipeline feeding both. Four issues were severe enough to fix immediately: a corrupted route table that had silently disabled two of three monitoring agents, an authorization gate that defaulted to off unless a specific environment variable happened to be set, a tenant-isolation check that could be skipped entirely, and an emergency “safe mode” that, as designed, would not have stopped anything.

All four were fixed the same night, applied to the live services, and verified against the running system — not just against unit tests. A second pass closed five smaller gaps: reviewer-independence, approval replay, a prompt/validator mismatch, injected-language detection, and a hard-coded path that had already caused one of the four critical bugs once.

Three findings remain open by design, not oversight — each is blocked on a decision already made to defer it (see Still open).

02

Critical & High findings

All four fixed, applied to the live services, and confirmed via /health and a full three-service safe-mode drill — twice.

▸ BOB‑REV‑01 Sentinel gate silently defaulted to allow, not deny Critical Fixed & live
Issue
Dispatcher only ran its authorization gate when BOB_SENTINEL_ENFORCEMENT=1 was set in its environment. Agent Worker already forced this in code; the dispatcher relied entirely on its launch script setting it — with no fail-closed fallback if it didn't.
Fix
Dispatcher's entry point now forces enforcement in code, the same way Agent Worker already did.
Verified
Restarted live; /health confirms sentinel.enabled: true independent of launch method. 14/14 dispatcher tests pass.
File
agent-dispatcher/src/main.js
▸ BOB‑REV‑02 Route table corrupted — two of three agents unreachable High Fixed & live
Issue
routes.json had been corrupted by a PowerShell serialization artifact — issues.* and pull_request.* rules were nested inside wrapper objects the router never inspected. Verified by executing the real router against the real file: only push events routed correctly. GitHub-Issue-Agent and PR-Review-Status-Agent had been silently dark, while the one working canary (a push event) made the whole pipeline look healthy.
Fix
Flattened the file's own rule objects back into a valid array — no rules added or removed, just un-nested.
Verified
Re-ran the router against the live file post-restart: issues.opened, pull_request.opened, and push all resolve to the correct agent.
File
webhook-runtime/config/routes.json
▸ BOB‑REV‑03 Tenant-boundary check skipped when tenant_id was empty High Fixed & live
Issue
The review gate's tenant check only ran when result.tenant_id was truthy — a missing tenant_id bypassed the check entirely, regardless of the reviewer's own tenant.
Fix
A missing or empty tenant_id is now a hard denial (tenant_unresolved), never a bypass.
Verified
Regression test added; 31/31 agent-worker tests pass on the live file.
File
agent-worker/src/review-gate.js
▸ BOB‑REV‑04 Emergency safe mode did not actually stop anything High Fixed & live
Issue
Safe mode only blocked write-class verbs (modify, send, delete, deploy, spend); this pipeline only ever requests read. And nothing in the pipeline ever set safeMode: true in the first place — there was no switch to flip.
Fix
Deliberately did not touch the shared verb semantics other code depends on. Instead added a dedicated kill-switch file, 01-Core\SAFE-MODE, checked by all three services on every sweep — the same pattern already used for each service's own DISABLED file, just shared across all three.
Verified
Full three-service drill, run twice (once before, once after the second remediation wave): baseline all enabled: true → file created, all three flip to false within 8 seconds → file removed, all three resume automatically.
File
01-Core/services/{webhook-runtime,agent-dispatcher,agent-worker}/src/main.js
03

Medium & Low findings

Second remediation wave — five self-contained fixes, none dependent on undecided architecture. All applied live and covered by new regression tests (31/31 passing).

▸ BOB‑REV‑08 Reviewer-independence check was a shallow string match Medium Fixed & live
Issue
Only blocked a reviewer whose principal ID matched the agent's own name string — not the human/process that actually triggered the event being reviewed.
Fix
The triggering principal now travels with the result as triggered_by; the gate rejects a reviewer who matches either the agent name or the trigger.
File
agent-worker/src/worker.js, review-gate.js
▸ BOB‑REV‑09 One approval could be replayed across duplicate results Medium Fixed & live
Issue
A retried delivery could produce two result files sharing one event_id; nothing stopped a single approval from releasing both.
Fix
New append-only ledger records every consumed approval evidence reference; a second release attempt is rejected as approval_already_consumed.
File
agent-worker/src/approval-ledger.js (new)
▸ BOB‑REV‑10 Model prompt didn't match what the validator checked for Low Fixed & live
Issue
The evidence validator checked for exact headings like Summary:, but the prompt never told the model to use that exact, colon-suffixed format — a latent source of false missing_section findings.
Fix
Prompt now spells out the exact three headings the validator checks for.
File
agent-worker/src/worker.js
▸ BOB‑REV‑11 Reviewer-directed language could ride along in analysis text Low Partially fixed
Issue
Untrusted GitHub content could contain injected phrases like “please approve this” that the model might echo into its own analysis — text a human reviewer would eventually read.
Fix (done)
Analysis text is now scanned for reviewer-directed imperative language; a match adds reviewer_directed_language_detected to the result's findings, independent of any UI.
Still open
Visibly demarcating untrusted content to a human reviewer needs an actual review UI, which doesn't exist yet — same blocker as REV‑06.
File
agent-worker/src/result-evidence.js
▸ BOB‑REV‑12 Deployment root hard-coded three times over Low Fixed & live
Issue
E:\BOB was duplicated identically across all three services' entry points — exactly the kind of drift that had already caused REV‑02.
Fix
One shared module now owns ROOT and the safe-mode file path; all three services import it.
File
01-Core/services/shared-config.js (new)
04

Live verification

Every fix was applied to the running services — not just tested in isolation — and the emergency switch was drilled end to end, twice.

Three-service SAFE-MODE drill · final run, post second remediation wave
Stagewebhook-runtimeagent-dispatcheragent-worker
Baselineenabledenabledenabled
SAFE-MODE engaged (+8s)disableddisableddisabled
SAFE-MODE cleared (+8s)enabledenabledenabled
Test suites, on the live files

agent-worker 31/31 · agent-dispatcher 14/14 · webhook-runtime 15/15 · shared-config 2/2

05

Audit log review

All three services' logs since Sept 17 (2,662 combined entries), plus every quarantine directory.

Clean: every event traces to the two allowlisted repositories. No unexpected agents or targets. Zero quarantine events in the entire history of any of the three services. No duplicate or replayed event IDs. No crash loops. No secrets in any current-format log line.

One finding — historical, not ongoing

The two oldest webhook-runtime log lines (Sept 17, from initial webhook setup) contain the full raw GitHub payload instead of the sanitized summary the current code writes — a leftover from before that day's hardening pass. They expose the webhook's public hostname, hooks.loper.family, and repo/owner metadata. GitHub's own redaction meant the actual signing secret was not exposed. Verified all 249 later entries, including today's, are properly sanitized — this affects two lines at rest, not current behavior.

06

Still open, by design

Not fixed tonight because each is blocked on a decision already made to defer it — not because they were missed.