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
+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>
<div className="agent-llm-state">
<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>
</header>
<div className="agent-tool-list">
+18 -25
View File
@@ -53,7 +53,7 @@ export default function Approvals() {
const [busy, setBusy] = useState<string | null>(null);
const tenantId = useApp((s) => s.tenantId);
const [runtimeHealth, setRuntimeHealth] = useState<"unknown" | "up" | "down">("unknown");
const [actionsEnabled, setActionsEnabled] = useState(false);
const loadQueue = async () => {
if (!tenantId) {
@@ -71,21 +71,6 @@ export default function Approvals() {
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(() => {
if (!activeKey) return;
let cancelled = false;
@@ -114,8 +99,8 @@ export default function Approvals() {
} catch (err: any) {
const msg = err.message || "";
if (/runtime-values|action_execution_failed|500/i.test(msg)) {
setRuntimeHealth("down");
pushToast("err", "The runtime engine couldn't accept that decision. Action buttons are now disabled until it recovers.");
setActionsEnabled(false);
pushToast("err", "The runtime engine couldn't accept that decision. Live actions have been turned back off.");
} else {
pushToast("err", "Something went wrong handling that action. Try again in a moment.");
}
@@ -198,21 +183,29 @@ export default function Approvals() {
</ul>
</div>
)}
{runtimeHealth === "down" && (
<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.
</div>
)}
<div className="approvals-runtime-note">
{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 className="approvals-actions">
{(tx.available_actions || []).map((a: any) => {
const disabled = !a.enabled || busy === a.id || runtimeHealth === "down";
const disabled = !a.enabled || busy === a.id || !actionsEnabled;
return (
<button
key={a.id}
className={`btn ${a.kind === "approve" || a.kind === "submit" ? "btn-primary" : "btn-secondary"}`}
disabled={disabled}
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}
</button>