fix(oracle-r8): approvals read-only by default + buyer-safe llm hint
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:
@@ -2141,3 +2141,11 @@ select.studio-input { background: var(--bp-paper); }
|
||||
.agent-llm-off { 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; }
|
||||
|
||||
.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; }
|
||||
|
||||
@@ -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">
|
||||
|
||||
+16
-23
@@ -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.
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user