Module: Rubino::Output::HeadlessBlockLatch
- Defined in:
- lib/rubino/output/headless_block_latch.rb
Overview
Process-global fail-closed latch for headless (‘rubino prompt`/-q) runs (F1-subagents).
The one-shot CLI reads the PARENT’s UI::Null#approval_blocked? after the run to decide the exit code (#260). But a ‘task` subagent runs on a SEPARATE, fresh UI::Null (nested_ui ⇒ Null off the CLI), so when the CHILD’s dangerous tool is fail-closed-blocked the latch lands on that DISCARDED child adapter — the parent’s UI::Null never sees it, and the CLI reported exit 0 + empty stderr even though the block held and the tool never ran. A direct ‘rubino prompt` of the same dangerous action correctly exits 2; routing it through a subagent silently looked like success, hiding the refusal from CI.
Fix: every UI::Null records a fail-closed block HERE too while a headless run is active, regardless of which (parent or child) adapter caught it. The one-shot exit check consults this latch in addition to the parent adapter, so a subagent-blocked headless run exits non-zero with the block notice on stderr — the same outcome as the direct run. Reset around each one-shot run so a long-lived embedder/test process doesn’t carry a stale block across invocations.
Class Method Summary collapse
-
.blocked? ⇒ Boolean
True when any tool was fail-closed-blocked anywhere in this headless run.
-
.messages ⇒ Object
The recorded block notices, in order, for the CLI to echo to stderr.
-
.record(message) ⇒ Object
Record a fail-closed block message.
-
.reset! ⇒ Object
Clear the latch.
Class Method Details
.blocked? ⇒ Boolean
True when any tool was fail-closed-blocked anywhere in this headless run.
37 38 39 |
# File 'lib/rubino/output/headless_block_latch.rb', line 37 def blocked? @mutex.synchronize { !@messages.empty? } end |
.messages ⇒ Object
The recorded block notices, in order, for the CLI to echo to stderr.
42 43 44 |
# File 'lib/rubino/output/headless_block_latch.rb', line 42 def @mutex.synchronize { @messages.dup } end |
.record(message) ⇒ Object
Record a fail-closed block message. Called from UI::Null#tool_blocked while Rubino.headless? — from the parent OR any (foreground) subagent.
32 33 34 |
# File 'lib/rubino/output/headless_block_latch.rb', line 32 def record() @mutex.synchronize { @messages << .to_s } end |
.reset! ⇒ Object
Clear the latch. Called at the start of each one-shot run so a reused process never inherits a stale block.
48 49 50 |
# File 'lib/rubino/output/headless_block_latch.rb', line 48 def reset! @mutex.synchronize { @messages = [] } end |