Class: Pikuri::Workspace::Confirmer
- Inherits:
-
Object
- Object
- Pikuri::Workspace::Confirmer
- Defined in:
- lib/pikuri/workspace/confirmer.rb,
lib/pikuri/workspace/confirmer/terminal.rb
Overview
Port for resolving a "may I do this?" confirmation to a decision —
currently Code::Bash, Write, and the agent tool's
delegation gate. Subclass and implement #ask.
The one job: resolve a Request to a decision
A Confirmer takes a semantic Request and returns Approved or Rejected. That is all: it carries no tool policy and never classifies the request's content (no filesystem reads, no command parsing, no notion of "passive"). Two shipped implementations:
- Terminal — the human chrome: shows the request and reads the answer.
In
confirmer/terminal.rbso this seam file stays free of rendering deps. Written for pikuri's own single-threadedbin/scripts and best read as the worked example — a TUI or web host implements this seam itself. - AutoApprove — the headless stance: ignores the request and blanket-approves (+--yolo+ / dev-container). Request-independent, so it encodes no policy either.
Sharing is per implementation, and the two shipped ones differ:
AutoApprove is P_stateless and shares freely, while Terminal is
P_one_agent because concurrent agents fight over one human's keystrokes.
A confirmer written for a multi-agent host owns that queueing — and owns
naming which agent is asking, which is why the queue can't usefully live
down here.
Content classification ("is this bash command provably passive, skip the
prompt?") deliberately does not live here — it's a tool concern
(Code::Bash takes a separate passive_detector: predicate,
falling through to this confirmer only for commands it can't pre-approve).
Keeping them apart lets one stateless chrome serve every tool.
The return type: Approved / Rejected
#ask returns Approved (the possibly-edited Request#detail to act on) or Rejected (an optional decline reason); callers pattern-match:
case confirmer.ask(request: request)
in Confirmer::Approved(new_request_detail:) then ...act on it...
in Confirmer::Rejected(reason:) then ...steer with it...
end
#confirm? is the derived boolean convenience for gates (Write, Code::ExitPlanMode) needing neither the payload nor the reason. Two orthogonal axes ride the richer return:
- Editing (opt-in via Request#editable) — when
detailis the verbatim actionable payload (theagenttask, wheredetail == task), a confirmer may let the human rewrite it — "human as author, not approver". Bash's"$ <command>"is a decorated display string, so it stays non-editable. - Reason (universal) — any decline can carry typed steering ("use the other library") back to the model as the next observation.
A +:once+/+:always+ scope knob was rejected: it manages decision
fatigue, a distinct axis, and the long-term answer to fatigue is making
confirmations rare (sandboxing, --yolo, the passive_detector skip),
not smarter approval modes. (An agentic destructive-or-not classifier is a
deferred v2.)
Seam discipline
Tools take a Confirmer via constructor and invoke #ask with a semantic Request — what is asked, never how it looks. All presentation belongs to the confirmer: color, the answer cue, parsing, the edit affordance, and — security-relevant — neutralizing hostile bytes in LLM-supplied text. The chrome-independent half (escape control bytes, flag bidi/zero-width/homoglyph spoofs) is the shared Sanitizer; the medium-specific half stays with the renderer (a terminal prints sanitized text; a web client HTML-escapes). Tools never call +gets+/+puts+ directly — keep IO at the seam so a TUI/web client plugs in without touching tools.
Defined Under Namespace
Classes: Approved, AutoApprove, Change, Rejected, Request, Terminal
Constant Summary collapse
- AUTO_APPROVE =
Shared stateless AutoApprove singleton, reusable across tools, sub-agents, and hosts' yolo modes. (No
TERMINALsingleton: Terminal is a demo chrome, and real front-ends bring their own.) AutoApprove.new
Instance Method Summary collapse
- #ask(request:) ⇒ Approved, Rejected
-
#blocks_on_human? ⇒ Boolean
Whether #ask can actually stop and wait for a person — the posture Trifecta reads when deciding whether a gated tool reaches the approver seat rather than counting as autonomous egress.
-
#confirm?(request:) ⇒ Boolean
Boolean convenience over #ask for gates needing neither the edited payload nor the reason.
Instance Method Details
#ask(request:) ⇒ Approved, Rejected
123 124 125 |
# File 'lib/pikuri/workspace/confirmer.rb', line 123 def ask(request:) raise NotImplementedError, "#{self.class}#ask must be implemented" end |
#blocks_on_human? ⇒ Boolean
Whether #ask can actually stop and wait for a person — the posture Trifecta reads when deciding whether a gated tool reaches the approver seat rather than counting as autonomous egress.
true on the base class: a confirmer exists to ask, so a subclass that
doesn't must say so, and one that forgets is credited with a gate it
does not have only if it also forgets to be AutoApprove. Override to
false for any request-independent auto-answer.
This is what makes --no-confirm re-harden a wiring's verdict on its
own, with nothing in the detector special-casing the flag.
149 |
# File 'lib/pikuri/workspace/confirmer.rb', line 149 def blocks_on_human? = true |