fix(oracle-r8): approvals read-only by default + buyer-safe llm hint
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

Two Oracle round-8 blockers:

1. Approvals action buttons stayed enabled even though the actual
   runtime engine submit path is known-500. The runtime-values GET
   probe proved EA2 reachability, not action-path readiness — Oracle
   rejected 'fail-once-then-disable' for a primary buyer CTA.

   Now: actionsEnabled starts FALSE. Approvals shows a 'Read-only
   view' note with an explicit 'Enable live actions' checkbox. Action
   buttons stay disabled until the operator opts in. If a click then
   500s, actionsEnabled flips back off and the toast says 'Live actions
   have been turned back off.' No first-click-into-error.

2. Agent OFF hint exposed raw env var names OPENAI_API_KEY /
   ANTHROPIC_API_KEY. Replaced with buyer-safe 'Natural language
   replies are not enabled in this environment.'

Also removed the runtimeHealth probe entirely (Oracle's nit: the
probe's semantics were misleading — it only proved reachability,
not action-path readiness).
This commit is contained in:
2026-06-14 17:33:04 +04:00
parent c6950e7699
commit 74ba4ab86f
3 changed files with 27 additions and 26 deletions
+8
View File
@@ -2141,3 +2141,11 @@ select.studio-input { background: var(--bp-paper); }
.agent-llm-off { color: var(--bp-muted); } .agent-llm-off { color: var(--bp-muted); }
.agent-llm-unknown { color: var(--bp-muted); } .agent-llm-unknown { color: var(--bp-muted); }
.agent-llm-hint { font-size: 11px; color: var(--bp-muted); line-height: 1.4; } .agent-llm-hint { font-size: 11px; color: var(--bp-muted); line-height: 1.4; }
.approvals-actions-toggle {
display: inline-flex; align-items: center; gap: 6px;
margin-left: 10px;
font-family: var(--bp-mono); font-size: 11px;
color: var(--bp-navy); cursor: pointer;
}
.approvals-actions-toggle input { cursor: pointer; }
+1 -1
View File
@@ -81,7 +81,7 @@ export default function Agent() {
<p className="agent-side-sub">Deterministic command router with EA2-backed text memory.</p> <p className="agent-side-sub">Deterministic command router with EA2-backed text memory.</p>
<div className="agent-llm-state"> <div className="agent-llm-state">
<span className={`agent-llm-pill agent-llm-${llmState}`}>LLM provider · {llmState === "ready" ? "ON" : llmState === "off" ? "OFF" : "…"}</span> <span className={`agent-llm-pill agent-llm-${llmState}`}>LLM provider · {llmState === "ready" ? "ON" : llmState === "off" ? "OFF" : "…"}</span>
{llmState === "off" && <span className="agent-llm-hint">Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable natural language replies.</span>} {llmState === "off" && <span className="agent-llm-hint">Natural language replies are not enabled in this environment.</span>}
</div> </div>
</header> </header>
<div className="agent-tool-list"> <div className="agent-tool-list">
+16 -23
View File
@@ -53,7 +53,7 @@ export default function Approvals() {
const [busy, setBusy] = useState<string | null>(null); const [busy, setBusy] = useState<string | null>(null);
const tenantId = useApp((s) => s.tenantId); const tenantId = useApp((s) => s.tenantId);
const [runtimeHealth, setRuntimeHealth] = useState<"unknown" | "up" | "down">("unknown"); const [actionsEnabled, setActionsEnabled] = useState(false);
const loadQueue = async () => { const loadQueue = async () => {
if (!tenantId) { if (!tenantId) {
@@ -71,21 +71,6 @@ export default function Approvals() {
useEffect(() => { void loadQueue(); }, [tenantId]); useEffect(() => { void loadQueue(); }, [tenantId]);
useEffect(() => {
let cancelled = false;
fetch(`${api.config.baseUrl}/api/ea2/runtime-values`, {
method: "GET",
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
})
.then((r) => {
if (cancelled) return;
if (r.status === 422 || r.ok) setRuntimeHealth("up");
else setRuntimeHealth("down");
})
.catch(() => { if (!cancelled) setRuntimeHealth("down"); });
return () => { cancelled = true; };
}, []);
useEffect(() => { useEffect(() => {
if (!activeKey) return; if (!activeKey) return;
let cancelled = false; let cancelled = false;
@@ -114,8 +99,8 @@ export default function Approvals() {
} catch (err: any) { } catch (err: any) {
const msg = err.message || ""; const msg = err.message || "";
if (/runtime-values|action_execution_failed|500/i.test(msg)) { if (/runtime-values|action_execution_failed|500/i.test(msg)) {
setRuntimeHealth("down"); setActionsEnabled(false);
pushToast("err", "The runtime engine couldn't accept that decision. Action buttons are now disabled until it recovers."); pushToast("err", "The runtime engine couldn't accept that decision. Live actions have been turned back off.");
} else { } else {
pushToast("err", "Something went wrong handling that action. Try again in a moment."); pushToast("err", "Something went wrong handling that action. Try again in a moment.");
} }
@@ -198,21 +183,29 @@ export default function Approvals() {
</ul> </ul>
</div> </div>
)} )}
{runtimeHealth === "down" && (
<div className="approvals-runtime-note"> <div className="approvals-runtime-note">
Runtime engine is unreachable from this environment. Action buttons are disabled. Use this view to inspect cases; once the runtime comes back, actions will write to EA2. {actionsEnabled
? "Live actions enabled. Clicking a button below writes the decision to EA2."
: "Read-only view. Inspect cases here; enable live actions to submit decisions to EA2."}
<label className="approvals-actions-toggle">
<input
type="checkbox"
checked={actionsEnabled}
onChange={(e) => setActionsEnabled(e.target.checked)}
/>
<span>Enable live actions</span>
</label>
</div> </div>
)}
<div className="approvals-actions"> <div className="approvals-actions">
{(tx.available_actions || []).map((a: any) => { {(tx.available_actions || []).map((a: any) => {
const disabled = !a.enabled || busy === a.id || runtimeHealth === "down"; const disabled = !a.enabled || busy === a.id || !actionsEnabled;
return ( return (
<button <button
key={a.id} key={a.id}
className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`} className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`}
disabled={disabled} disabled={disabled}
onClick={() => handleAction(a.id)} onClick={() => handleAction(a.id)}
title={runtimeHealth === "down" ? "Runtime engine offline" : a.display_label || a.id} title={actionsEnabled ? a.display_label || a.id : "Enable live actions to submit"}
> >
<Pulse size={11} /> {busy === a.id ? "Sending…" : a.display_label || a.id} <Pulse size={11} /> {busy === a.id ? "Sending…" : a.display_label || a.id}
</button> </button>