fix(curation): kill dev-artefact leak + rename agent honestly

Oracle remediation batch 1 — closing gaps #1 (agent overclaim), #4
(demo-data leak), #5 (raw-ID exposure):

- src/lib/flowCuration.ts: shared isDevArtefact + curatedPublishedFlows
  filter for the whole codebase. Patterns drop rv_/orphan/Atlas F1/MCP
  SDX/Codex Test/sidekick/process_<ts>/backfill/smoke/probe/fixture and
  the EA2_* internal source_contexts.
- src/scenes/Hub.tsx: loadPublishedFlows uses the shared curator
- src/lib/agentTools.ts: list_processes and start_process both use the
  shared curator; raw transaction_id no longer in start reply
- src/state/store.ts: snapshot + live merges curated; resolveDefaultStepId
  guards against scenarios whose defaultStepId references a stale step
- src/scenes/Wizard.tsx: 'Process definition <hex>' confirmation replaced
  with the user-facing process name
- src/scenes/Hub.tsx: start toast no longer shows transaction_id prefix
- src/scenes/Agent.tsx + App.tsx + Landing.tsx: rename Pi -> FlowMaster
  Command Assistant. Welcome message says explicitly that this is a
  deterministic router and the LLM agent + tenant memory are on the
  roadmap. Tab label, hub chip, turn author all rebranded.
- src/scenes/GeoAttendance.tsx: scene re-titled to 'Attendance map ·
  preview' with honest copy explaining the dataset is illustrative until
  the EA2 attendance feed is wired in.
- src/data/synthetic.ts: orphaned from runtime (kept in tree for tests).
  Snapshot now sources only the live-cached EA2 read (scenarios.json),
  filtered through the same curator.

29/29 tests green. No more 'rv test flow 0' / 'orphan' / 'Atlas F1' / raw
32-char hex IDs visible to a buyer.
This commit is contained in:
2026-06-14 13:11:54 +04:00
parent 7ceb5c05bb
commit 8933148719
9 changed files with 109 additions and 48 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export default function App() {
<Bot size={13} /> Chat <Bot size={13} /> Chat
</button> </button>
<button role="tab" aria-selected={scene === "agent"} className={`tab${scene === "agent" ? " tab-sel" : ""}`} onClick={() => setScene("agent")}> <button role="tab" aria-selected={scene === "agent"} className={`tab${scene === "agent" ? " tab-sel" : ""}`} onClick={() => setScene("agent")}>
<Bot size={13} /> Pi <Bot size={13} /> Assistant
</button> </button>
<button role="tab" aria-selected={scene === "settings"} className={`tab${scene === "settings" ? " tab-sel" : ""}`} onClick={() => setScene("settings")}> <button role="tab" aria-selected={scene === "settings"} className={`tab${scene === "settings" ? " tab-sel" : ""}`} onClick={() => setScene("settings")}>
<Cog size={13} /> Settings <Cog size={13} /> Settings
+11 -10
View File
@@ -1,6 +1,7 @@
import { api } from "./api"; import { api } from "./api";
import { wizardApi } from "./wizardApi"; import { wizardApi } from "./wizardApi";
import { chatApi } from "./chatApi"; import { chatApi } from "./chatApi";
import { curatedPublishedFlows } from "./flowCuration";
export interface ToolResult { export interface ToolResult {
ok: boolean; ok: boolean;
@@ -72,19 +73,17 @@ const TOOLS: ToolDef[] = [
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
}).then((r) => r.json()); }).then((r) => r.json());
const items: any[] = procs?.items || []; const items = curatedPublishedFlows((procs?.items || []) as any[]);
const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
const target = norm(processName); const target = norm(processName);
const match = items const match = items.find((it) => norm(it.display_name || "").includes(target) || norm(it.name || "").includes(target));
.filter((it) => it.status === "published" && it.kind === "definition")
.find((it) => norm(it.display_name || "").includes(target) || norm(it.name || "").includes(target));
if (!match) return { ok: false, display: `Couldn't find a published process matching "${processName}". Try the Studio to create one.` }; if (!match) return { ok: false, display: `Couldn't find a published process matching "${processName}". Try the Studio to create one.` };
try { try {
const res = await wizardApi.startInstance(match._key, subject); const res = await wizardApi.startInstance(match._key, subject);
return { return {
ok: true, ok: true,
display: `Started ${match.display_name}${subject ? ` for ${subject}` : ""}. Transaction ${res.transaction_id?.slice(0, 8) || "ok"}.`, display: `Started ${match.display_name}${subject ? ` for ${subject}` : ""}.`,
data: res, data: res,
}; };
} catch (e: any) { } catch (e: any) {
@@ -129,12 +128,14 @@ const TOOLS: ToolDef[] = [
description: "List published processes for this tenant", description: "List published processes for this tenant",
matcher: /^(list|show|what)\s+(processes|workflows|flows)/i, matcher: /^(list|show|what)\s+(processes|workflows|flows)/i,
async run() { async run() {
const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=20`, { const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
}).then((r) => r.json()); })
const published = (procs?.items || []).filter((it: any) => it.status === "published" && it.kind === "definition").slice(0, 10); .then((r) => r.json())
if (!published.length) return { ok: true, display: "No published processes yet. Build one in the Studio." }; .catch(() => ({ items: [] }));
const lines = published.map((it: any) => `${it.display_name || it.name}`).join("\n"); const published = curatedPublishedFlows((procs?.items || []) as any[]).slice(0, 10);
if (!published.length) return { ok: true, display: "No published processes yet. Open the Studio to design one." };
const lines = published.map((it) => `${it.display_name}`).join("\n");
return { ok: true, display: `Here's what's published:\n${lines}` }; return { ok: true, display: `Here's what's published:\n${lines}` };
}, },
}, },
+58
View File
@@ -0,0 +1,58 @@
export interface RawFlow {
_key: string;
display_name?: string;
name?: string;
description?: string;
source_context?: string;
status?: string;
kind?: string;
}
const DEV_ARTEFACT_PATTERNS = [
/^rv[_\s]/i,
/^orphan/i,
/^atlas[_\s]?f1/i,
/\batlas-f1-fresh\b/i,
/mcp[_\s]?sdx[_\s]?smoke/i,
/\bcodex[_\s]?test\b/i,
/\bsidekick\b/i,
/^process_\d{10,}$/i,
/\bbackfill\b/i,
/\bsmoke[_\s]?test\b/i,
/\bprobe\b/i,
/\bfixture\b/i,
];
const NON_BUSINESS_SOURCE_CONTEXTS = new Set([
"EA2_CHAT_THREAD",
"EA2_CHAT_MSG",
"EA2_WIZARD_STEP",
"EA2_DRAFT_PROCESS:process_creation",
]);
const HEX32 = /[a-f0-9]{32}/gi;
const HEX_SUFFIX = /[_-][a-f0-9]{8,}$/i;
export function isDevArtefact(f: RawFlow): boolean {
if (f.source_context && NON_BUSINESS_SOURCE_CONTEXTS.has(f.source_context)) return true;
const haystack = `${f.display_name || ""} ${f.name || ""} ${f.description || ""}`;
return DEV_ARTEFACT_PATTERNS.some((p) => p.test(haystack));
}
export function curatedPublishedFlows<T extends RawFlow>(items: T[]): T[] {
return items.filter(
(it) => it.status === "published" && it.kind === "definition" && !!it.display_name && !isDevArtefact(it)
);
}
export function friendlyShortId(id: string | undefined | null): string {
if (!id) return "";
const trimmed = id.replace(HEX_SUFFIX, "");
if (trimmed.length < id.length && trimmed.length > 0) return trimmed;
if (HEX32.test(id)) return `ref-${id.slice(0, 4)}`;
return id;
}
export function scrubIdsFromText(text: string): string {
return text.replace(HEX32, "");
}
+6 -5
View File
@@ -28,7 +28,7 @@ export default function Agent() {
role: "agent", role: "agent",
ok: true, ok: true,
text: text:
`Hi ${userEmail.split("@")[0]}. I'm Pi — your FlowMaster co-pilot. I can navigate the cockpit, start processes, message your team, and walk you through the Studio.`, `Hi ${userEmail.split("@")[0]}. I'm the FlowMaster Command Assistant. I understand a fixed set of commands today and run them against the real EA2 backend — navigate, list processes, start a process, message a teammate, open the Studio. A natural-language LLM and per-tenant memory are on the roadmap.`,
}, },
]); ]);
const [draft, setDraft] = useState(""); const [draft, setDraft] = useState("");
@@ -63,8 +63,9 @@ export default function Agent() {
<div className="agent-scene"> <div className="agent-scene">
<aside className="agent-sidebar"> <aside className="agent-sidebar">
<header className="agent-side-head"> <header className="agent-side-head">
<div className="mc-hero-eyebrow"><Bot size={12} /> Pi FlowMaster Agent</div> <div className="mc-hero-eyebrow"><Bot size={12} /> FlowMaster Command Assistant</div>
<h2 className="mc-hero-title">Talk to your cockpit</h2> <h2 className="mc-hero-title">Tell the cockpit what to do</h2>
<p className="agent-side-sub">Deterministic command router today. An LLM-backed agent with per-tenant memory is the next milestone.</p>
</header> </header>
<div className="agent-tool-list"> <div className="agent-tool-list">
<div className="agent-tool-label">CAPABILITIES</div> <div className="agent-tool-label">CAPABILITIES</div>
@@ -84,11 +85,11 @@ export default function Agent() {
<div className="agent-messages" ref={scrollRef}> <div className="agent-messages" ref={scrollRef}>
{turns.map((t) => ( {turns.map((t) => (
<div key={t.id} className={`agent-turn agent-turn-${t.role}${t.ok === false ? " agent-turn-err" : ""}`}> <div key={t.id} className={`agent-turn agent-turn-${t.role}${t.ok === false ? " agent-turn-err" : ""}`}>
<div className="agent-turn-author">{t.role === "user" ? userEmail.split("@")[0] : "Pi"}</div> <div className="agent-turn-author">{t.role === "user" ? userEmail.split("@")[0] : "Assistant"}</div>
<div className="agent-turn-body">{t.text}</div> <div className="agent-turn-body">{t.text}</div>
</div> </div>
))} ))}
{working && <div className="agent-turn agent-turn-agent agent-turn-thinking">Pi is thinking</div>} {working && <div className="agent-turn agent-turn-agent agent-turn-thinking">Working</div>}
</div> </div>
{turns.length <= 1 && ( {turns.length <= 1 && (
+3 -3
View File
@@ -69,10 +69,10 @@ export default function GeoAttendance() {
return ( return (
<div className="geo-scene"> <div className="geo-scene">
<header className="geo-head"> <header className="geo-head">
<div className="mc-hero-eyebrow"><Pulse size={12} /> Real-time attendance</div> <div className="mc-hero-eyebrow"><Pulse size={12} /> Attendance map · preview</div>
<h2 className="mc-hero-title">Where your people are right now</h2> <h2 className="mc-hero-title">Where your people are</h2>
<p className="geo-intro"> <p className="geo-intro">
Live attendance for stores, offices, and warehouses. Click a marker to see who's checked in and when. Powered by OpenStreetMap tiles — no proprietary mapping key required. Preview of the manager attendance view. Markers are illustrative until the attendance feed is wired up against the EA2 runtime; once connected, statuses come from real check-in events. Tiles are served by OpenStreetMap.
</p> </p>
<div className="geo-stats"> <div className="geo-stats">
<span className="geo-stat geo-stat-ok">{stats.onSite} on-site</span> <span className="geo-stat geo-stat-ok">{stats.onSite} on-site</span>
+5 -17
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useApp } from "../state/store"; import { useApp } from "../state/store";
import { api } from "../lib/api"; import { api } from "../lib/api";
import { wizardApi } from "../lib/wizardApi"; import { wizardApi } from "../lib/wizardApi";
import { curatedPublishedFlows } from "../lib/flowCuration";
import { Branch, Layers, Pulse } from "../components/icons"; import { Branch, Layers, Pulse } from "../components/icons";
export type HubKey = "procurement" | "hr" | "it"; export type HubKey = "procurement" | "hr" | "it";
@@ -57,29 +58,15 @@ interface PublishedFlow {
name?: string; name?: string;
} }
const NON_BUSINESS_SOURCE_CONTEXTS = new Set([
"EA2_CHAT_THREAD",
"EA2_CHAT_MSG",
"EA2_WIZARD_STEP",
]);
async function loadPublishedFlows(): Promise<PublishedFlow[]> { async function loadPublishedFlows(): Promise<PublishedFlow[]> {
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
}); });
if (!res.ok) throw new Error(`processes ${res.status}`); if (!res.ok) throw new Error(`processes ${res.status}`);
const body = await res.json(); const body = await res.json();
return ((body?.items || []) as any[]) return curatedPublishedFlows((body?.items || []) as any[]).map((it) => ({
.filter(
(it) =>
it.status === "published" &&
it.kind === "definition" &&
it.display_name &&
!NON_BUSINESS_SOURCE_CONTEXTS.has(it.source_context)
)
.map((it) => ({
_key: it._key, _key: it._key,
display_name: it.display_name, display_name: it.display_name!,
description: it.description, description: it.description,
name: it.name, name: it.name,
})); }));
@@ -110,7 +97,8 @@ export default function Hub({ hub }: { hub: HubKey }) {
setBusy(flowKey); setBusy(flowKey);
try { try {
const res = await wizardApi.startInstance(flowKey, subject); const res = await wizardApi.startInstance(flowKey, subject);
pushToast("ok", `Started ${label}${subject ? ` for ${subject}` : ""}${res.transaction_id?.slice(0, 8) || "ok"}`); void res;
pushToast("ok", `Started ${label}${subject ? ` for ${subject}` : ""}.`);
setScene("mission"); setScene("mission");
} catch (err: any) { } catch (err: any) {
pushToast("err", `Start failed: ${err.message}`); pushToast("err", `Start failed: ${err.message}`);
+1 -1
View File
@@ -80,7 +80,7 @@ export default function Landing() {
<button className="hub-chip" onClick={() => setScene("hub-hr")}>People Hub</button> <button className="hub-chip" onClick={() => setScene("hub-hr")}>People Hub</button>
<button className="hub-chip" onClick={() => setScene("hub-it")}>IT Hub</button> <button className="hub-chip" onClick={() => setScene("hub-it")}>IT Hub</button>
<button className="hub-chip" onClick={() => setScene("geo-attendance")}>Attendance Map</button> <button className="hub-chip" onClick={() => setScene("geo-attendance")}>Attendance Map</button>
<button className="hub-chip" onClick={() => setScene("agent")}>Talk to Pi</button> <button className="hub-chip" onClick={() => setScene("agent")}>Command Assistant</button>
<button className="hub-chip" onClick={() => setScene("chat")}>Team Chat</button> <button className="hub-chip" onClick={() => setScene("chat")}>Team Chat</button>
<button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button> <button className="hub-chip" onClick={() => setScene("explainer")}>What is FlowMaster?</button>
</div> </div>
+1 -1
View File
@@ -403,7 +403,7 @@ export default function Wizard() {
<div className="wizard-panel success-panel"> <div className="wizard-panel success-panel">
<div className="success-icon"><Check size={32} /></div> <div className="success-icon"><Check size={32} /></div>
<h3 className="panel-h">Process Published!</h3> <h3 className="panel-h">Process Published!</h3>
<p>Process definition <code>{publishedKey}</code> is now active in EA2.</p> <p>Your process <strong>{draft.name || "Untitled"}</strong> is now active and ready to run.</p>
<div className="actions-row"> <div className="actions-row">
<button className="btn btn-secondary" onClick={() => { setDraft({ step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }); setPublishedKey(null); }}>Create Another</button> <button className="btn btn-secondary" onClick={() => { setDraft({ step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }); setPublishedKey(null); }}>Create Another</button>
<button <button
+19 -6
View File
@@ -1,7 +1,19 @@
// Global UI state for Mission Control. // Global UI state for Mission Control.
import { create } from "zustand"; import { create } from "zustand";
import { liveScenarios as snapshotLive } from "../data/live"; import { liveScenarios as snapshotLive } from "../data/live";
import { syntheticScenarios } from "../data/synthetic"; import { isDevArtefact } from "../lib/flowCuration";
const syntheticScenarios: any[] = [];
function curateScenarios(rows: ProcessScenario[]): ProcessScenario[] {
return rows.filter((s) => !isDevArtefact({ _key: s.defKey, display_name: s.defName, name: s.defKey }));
}
function resolveDefaultStepId(scenario: ProcessScenario | undefined): string | null {
if (!scenario) return null;
const ids = new Set(scenario.steps.map((s) => s.id));
if (scenario.defaultStepId && ids.has(scenario.defaultStepId)) return scenario.defaultStepId;
return scenario.steps[0]?.id ?? null;
}
import { buildLiveScenariosFromApi } from "../lib/buildScenarios"; import { buildLiveScenariosFromApi } from "../lib/buildScenarios";
import { api, type ApiCall, type Actor } from "../lib/api"; import { api, type ApiCall, type Actor } from "../lib/api";
import type { ProcessScenario } from "../data/types"; import type { ProcessScenario } from "../data/types";
@@ -110,7 +122,7 @@ interface AppState {
startInstance: (defKey: string, businessSubject?: string) => Promise<string | null>; startInstance: (defKey: string, businessSubject?: string) => Promise<string | null>;
} }
const SNAPSHOT_SCENARIOS: ProcessScenario[] = [...snapshotLive, ...syntheticScenarios]; const SNAPSHOT_SCENARIOS: ProcessScenario[] = curateScenarios([...snapshotLive, ...syntheticScenarios]);
const initialScenario = SNAPSHOT_SCENARIOS[0]; const initialScenario = SNAPSHOT_SCENARIOS[0];
let toastSeq = 0; let toastSeq = 0;
@@ -132,7 +144,8 @@ async function runLiveFetch(
}); });
} }
const { scenarios, workItems, distinctDefs } = await buildLiveScenariosFromApi(); const { scenarios, workItems, distinctDefs } = await buildLiveScenariosFromApi();
const merged = [...scenarios, ...syntheticScenarios]; const curatedLive = curateScenarios([...scenarios, ...syntheticScenarios]);
const merged = curatedLive.length > 0 ? curatedLive : SNAPSHOT_SCENARIOS;
const first = merged[0]; const first = merged[0];
const currentId = get().scenarioId; const currentId = get().scenarioId;
const currentStepId = get().selectedStepId; const currentStepId = get().selectedStepId;
@@ -145,13 +158,13 @@ async function runLiveFetch(
liveFetchedAt: Date.now(), liveFetchedAt: Date.now(),
liveLoading: false, liveLoading: false,
scenarioId: nextScenario?.id ?? currentId, scenarioId: nextScenario?.id ?? currentId,
selectedStepId: stepStillThere ? currentStepId : nextScenario?.defaultStepId ?? null, selectedStepId: stepStillThere ? currentStepId : resolveDefaultStepId(nextScenario),
}); });
get().pushToast( get().pushToast(
"ok", "ok",
prevMode === "live" prevMode === "live"
? `Refreshed · ${scenarios.length} live + ${syntheticScenarios.length} blueprint scenarios` ? `Refreshed · ${scenarios.length} live processes`
: `Live mode · ${scenarios.length} live + ${syntheticScenarios.length} blueprint scenarios`, : `Live mode · ${scenarios.length} live processes`,
); );
} catch (e) { } catch (e) {
set({ liveLoading: false, liveError: (e as Error).message, mode: "snapshot", scenarios: SNAPSHOT_SCENARIOS }); set({ liveLoading: false, liveError: (e as Error).message, mode: "snapshot", scenarios: SNAPSHOT_SCENARIOS });