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-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; }
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user