@assistant-ui/react-devtools 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DevToolsFrame.js +1 -1
- package/dist/DevToolsHost.d.ts +1 -0
- package/dist/DevToolsHost.d.ts.map +1 -1
- package/dist/DevToolsHost.js +15 -5
- package/dist/DevToolsHost.js.map +1 -1
- package/dist/DevToolsModal.d.ts +1 -1
- package/dist/DevToolsModal.d.ts.map +1 -1
- package/dist/DevToolsModal.js +1 -1
- package/dist/DevToolsModal.js.map +1 -1
- package/dist/ExtensionHost.d.ts.map +1 -1
- package/dist/FrameClient.d.ts +2 -1
- package/dist/FrameClient.d.ts.map +1 -1
- package/dist/FrameClient.js.map +1 -1
- package/dist/FrameHost.d.ts.map +1 -1
- package/dist/utils/serialization.d.ts +11 -1
- package/dist/utils/serialization.d.ts.map +1 -1
- package/dist/utils/serialization.js +62 -7
- package/dist/utils/serialization.js.map +1 -1
- package/dist/utils/toolNormalization.d.ts +7 -0
- package/dist/utils/toolNormalization.d.ts.map +1 -1
- package/dist/utils/toolNormalization.js +7 -0
- package/dist/utils/toolNormalization.js.map +1 -1
- package/package.json +13 -11
- package/src/DevToolsHost.ts +32 -4
- package/src/DevToolsModal.tsx +0 -2
- package/src/FrameClient.ts +2 -3
- package/src/utils/serialization.test.ts +128 -0
- package/src/utils/serialization.ts +84 -7
- package/src/utils/toolNormalization.test.ts +71 -0
- package/src/utils/toolNormalization.ts +35 -0
package/dist/DevToolsFrame.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { FrameHost } from "./FrameHost.js";
|
|
3
3
|
import { DEFAULT_FRAME_URL } from "./constants.js";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
//#region src/DevToolsFrame.tsx
|
|
7
7
|
const DevToolsFrame = ({ frameUrl = DEFAULT_FRAME_URL, className, style, title = "assistant-ui DevTools" }) => {
|
package/dist/DevToolsHost.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevToolsHost.d.ts","names":[],"sources":["../src/DevToolsHost.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"DevToolsHost.d.ts","names":[],"sources":["../src/DevToolsHost.ts"],"mappings":";UAOU,kBAAA;EACR,IAAA;EACA,IAAA;IACE,OAAA;IACA,IAAA;IACA,KAAA;EAAA;AAAA;AAAA,UAIM,kBAAA;EACR,IAAA;EACA,IAAA;IACE,OAAA,GAAU,KAAA;MAAQ,KAAA;IAAA;IAClB,IAAA,GAAO,KAAK;MACV,KAAA;MACA,KAAA;MACA,MAAA;MACA,YAAA;MACA,MAAA;IAAA;EAAA;AAAA;AAAA,cAKO,YAAA;EAAA,QACH,YAAA;EAAA,QAOA,WAAA;EAAA,QACA,aAAA;cAEI,aAAA,GAAgB,OAAA,EAAS,kBAAA;EAKrC,gBAAA,CAAiB,OAAA,EAAS,kBAAkB;EAAA,QAcpC,kBAAA;EAAA,QAiBA,mBAAA;EAAA,QAMA,UAAA;EAyFR,OAAA;AAAA"}
|
package/dist/DevToolsHost.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeForMessage, serializeModelContext } from "./utils/serialization.js";
|
|
1
|
+
import { sanitizeAndRedact, sanitizeForMessage, serializeModelContext } from "./utils/serialization.js";
|
|
2
2
|
import { DevToolsHooks } from "@assistant-ui/react";
|
|
3
3
|
//#region src/DevToolsHost.ts
|
|
4
4
|
var DevToolsHost = class {
|
|
@@ -52,12 +52,21 @@ var DevToolsHost = class {
|
|
|
52
52
|
const apiEntry = allApis.get(apiId);
|
|
53
53
|
if (apiEntry) {
|
|
54
54
|
const state = {};
|
|
55
|
+
const scopes = [];
|
|
55
56
|
if (apiEntry.api) {
|
|
56
57
|
for (const [name, scope] of Object.entries(apiEntry.api)) if (typeof scope === "function" && "source" in scope) {
|
|
57
|
-
|
|
58
|
+
let methods = [];
|
|
59
|
+
try {
|
|
58
60
|
const scopeValue = scope();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (scopeValue && typeof scopeValue === "object") methods = Object.keys(scopeValue).filter((key) => typeof scopeValue[key] === "function");
|
|
62
|
+
if (scope.source === "root") state[name] = scopeValue?.getState?.() ?? scopeValue;
|
|
63
|
+
} catch {}
|
|
64
|
+
scopes.push({
|
|
65
|
+
name,
|
|
66
|
+
source: scope.source,
|
|
67
|
+
query: scope.query,
|
|
68
|
+
methods
|
|
69
|
+
});
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
const modelContext = serializeModelContext(apiEntry.api?.thread?.().getModelContext());
|
|
@@ -65,7 +74,8 @@ var DevToolsHost = class {
|
|
|
65
74
|
apiId,
|
|
66
75
|
state: sanitizeForMessage(state),
|
|
67
76
|
events: sanitizeForMessage(apiEntry.logs),
|
|
68
|
-
modelContext
|
|
77
|
+
modelContext,
|
|
78
|
+
scopes: sanitizeAndRedact(scopes)
|
|
69
79
|
});
|
|
70
80
|
}
|
|
71
81
|
}
|
package/dist/DevToolsHost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevToolsHost.js","names":[],"sources":["../src/DevToolsHost.ts"],"sourcesContent":["import { DevToolsHooks } from \"@assistant-ui/react\";\nimport {\n sanitizeForMessage,\n serializeModelContext,\n} from \"./utils/serialization\";\n\ninterface FrameToHostMessage {\n type: \"subscription\" | \"clearEvents\";\n data: {\n apiList?: boolean;\n apis?: number[];\n apiId?: number;\n };\n}\n\ninterface HostToFrameMessage {\n type: \"update\";\n data: {\n apiList?: Array<{ apiId: number }>;\n apis?: Array<{\n apiId: number;\n state: any;\n events: any[];\n modelContext?: any;\n }>;\n };\n}\n\nexport class DevToolsHost {\n private subscription: {\n apiList: boolean;\n apis: Set<number>;\n } = {\n apiList: false,\n apis: new Set(),\n };\n private unsubscribe?: () => void;\n private onSendMessage: (message: HostToFrameMessage) => void;\n\n constructor(onSendMessage: (message: HostToFrameMessage) => void) {\n this.onSendMessage = onSendMessage;\n this.subscribeToDevTools();\n }\n\n onReceiveMessage(message: FrameToHostMessage) {\n switch (message.type) {\n case \"subscription\":\n this.handleSubscription(message.data);\n break;\n case \"clearEvents\":\n if (typeof message.data.apiId === \"number\") {\n DevToolsHooks.clearEventLogs(message.data.apiId);\n // The subscription will automatically trigger an update\n }\n break;\n }\n }\n\n private handleSubscription(data: FrameToHostMessage[\"data\"]) {\n const prevApiList = this.subscription.apiList;\n const prevApis = new Set(this.subscription.apis);\n\n this.subscription.apiList = data.apiList || false;\n this.subscription.apis = new Set(data.apis);\n\n // Only send update if subscription actually changed\n const apisChanged =\n prevApis.size !== this.subscription.apis.size ||\n [...this.subscription.apis].some((id) => !prevApis.has(id));\n\n if (prevApiList !== this.subscription.apiList || apisChanged) {\n this.sendUpdate();\n }\n }\n\n private subscribeToDevTools() {\n this.unsubscribe = DevToolsHooks.subscribe(() => {\n this.sendUpdate();\n });\n }\n\n private sendUpdate() {\n const update: HostToFrameMessage = {\n type: \"update\",\n data: {},\n };\n\n const allApis = DevToolsHooks.getApis();\n for (const subscriptionApiId of this.subscription.apis) {\n if (!allApis.has(subscriptionApiId)) {\n this.subscription.apis.delete(subscriptionApiId);\n }\n }\n\n if (this.subscription.apiList) {\n update.data.apiList = [...allApis.keys()].map((apiId) => ({ apiId }));\n\n if (this.subscription.apis.size === 0 && allApis.size > 0) {\n this.subscription.apis = new Set([allApis.keys().next().value!]);\n }\n }\n\n if (this.subscription.apis.size > 0) {\n update.data.apis = [];\n\n for (const apiId of this.subscription.apis) {\n const apiEntry = allApis.get(apiId);\n if (apiEntry) {\n // Collect state
|
|
1
|
+
{"version":3,"file":"DevToolsHost.js","names":[],"sources":["../src/DevToolsHost.ts"],"sourcesContent":["import { DevToolsHooks } from \"@assistant-ui/react\";\nimport {\n sanitizeAndRedact,\n sanitizeForMessage,\n serializeModelContext,\n} from \"./utils/serialization\";\n\ninterface FrameToHostMessage {\n type: \"subscription\" | \"clearEvents\";\n data: {\n apiList?: boolean;\n apis?: number[];\n apiId?: number;\n };\n}\n\ninterface HostToFrameMessage {\n type: \"update\";\n data: {\n apiList?: Array<{ apiId: number }>;\n apis?: Array<{\n apiId: number;\n state: any;\n events: any[];\n modelContext?: any;\n scopes?: any;\n }>;\n };\n}\n\nexport class DevToolsHost {\n private subscription: {\n apiList: boolean;\n apis: Set<number>;\n } = {\n apiList: false,\n apis: new Set(),\n };\n private unsubscribe?: () => void;\n private onSendMessage: (message: HostToFrameMessage) => void;\n\n constructor(onSendMessage: (message: HostToFrameMessage) => void) {\n this.onSendMessage = onSendMessage;\n this.subscribeToDevTools();\n }\n\n onReceiveMessage(message: FrameToHostMessage) {\n switch (message.type) {\n case \"subscription\":\n this.handleSubscription(message.data);\n break;\n case \"clearEvents\":\n if (typeof message.data.apiId === \"number\") {\n DevToolsHooks.clearEventLogs(message.data.apiId);\n // The subscription will automatically trigger an update\n }\n break;\n }\n }\n\n private handleSubscription(data: FrameToHostMessage[\"data\"]) {\n const prevApiList = this.subscription.apiList;\n const prevApis = new Set(this.subscription.apis);\n\n this.subscription.apiList = data.apiList || false;\n this.subscription.apis = new Set(data.apis);\n\n // Only send update if subscription actually changed\n const apisChanged =\n prevApis.size !== this.subscription.apis.size ||\n [...this.subscription.apis].some((id) => !prevApis.has(id));\n\n if (prevApiList !== this.subscription.apiList || apisChanged) {\n this.sendUpdate();\n }\n }\n\n private subscribeToDevTools() {\n this.unsubscribe = DevToolsHooks.subscribe(() => {\n this.sendUpdate();\n });\n }\n\n private sendUpdate() {\n const update: HostToFrameMessage = {\n type: \"update\",\n data: {},\n };\n\n const allApis = DevToolsHooks.getApis();\n for (const subscriptionApiId of this.subscription.apis) {\n if (!allApis.has(subscriptionApiId)) {\n this.subscription.apis.delete(subscriptionApiId);\n }\n }\n\n if (this.subscription.apiList) {\n update.data.apiList = [...allApis.keys()].map((apiId) => ({ apiId }));\n\n if (this.subscription.apis.size === 0 && allApis.size > 0) {\n this.subscription.apis = new Set([allApis.keys().next().value!]);\n }\n }\n\n if (this.subscription.apis.size > 0) {\n update.data.apis = [];\n\n for (const apiId of this.subscription.apis) {\n const apiEntry = allApis.get(apiId);\n if (apiEntry) {\n // Collect root-scope state plus the identity and method catalog of\n // every accessor (root and derived) for the scope graph.\n const state: Record<string, unknown> = {};\n const scopes: Array<{\n name: string;\n source: string | null;\n query: Record<string, unknown> | null;\n methods: string[];\n }> = [];\n if (apiEntry.api) {\n for (const [name, scope] of Object.entries(apiEntry.api)) {\n if (typeof scope === \"function\" && \"source\" in scope) {\n let methods: string[] = [];\n try {\n const scopeValue = scope();\n if (scopeValue && typeof scopeValue === \"object\") {\n methods = Object.keys(scopeValue).filter(\n (key) =>\n typeof (scopeValue as Record<string, unknown>)[key] ===\n \"function\",\n );\n }\n if (scope.source === \"root\") {\n state[name] = scopeValue?.getState?.() ?? scopeValue;\n }\n } catch {\n // scope() may throw (derived scopes before the client is\n // mounted, or a broken root scope implementation).\n }\n scopes.push({\n name,\n source: scope.source,\n query: scope.query,\n methods,\n });\n }\n }\n }\n\n // Extract model context from thread runtime\n const modelContext = serializeModelContext(\n apiEntry.api?.thread?.().getModelContext(),\n );\n\n update.data.apis.push({\n apiId,\n state: sanitizeForMessage(state),\n events: sanitizeForMessage(apiEntry.logs) as unknown[],\n modelContext: modelContext,\n scopes: sanitizeAndRedact(scopes),\n });\n }\n }\n }\n\n if (Object.keys(update.data).length === 0) {\n return;\n }\n\n this.onSendMessage(update);\n }\n\n destroy() {\n if (this.unsubscribe) {\n this.unsubscribe();\n }\n }\n}\n"],"mappings":";;;AA8BA,IAAa,eAAb,MAA0B;CACxB,eAGI;EACF,SAAS;EACT,sBAAM,IAAI,IAAI;CAChB;CACA;CACA;CAEA,YAAY,eAAsD;EAChE,KAAK,gBAAgB;EACrB,KAAK,oBAAoB;CAC3B;CAEA,iBAAiB,SAA6B;EAC5C,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,KAAK,mBAAmB,QAAQ,IAAI;IACpC;GACF,KAAK;IACH,IAAI,OAAO,QAAQ,KAAK,UAAU,UAChC,cAAc,eAAe,QAAQ,KAAK,KAAK;IAGjD;EACJ;CACF;CAEA,mBAA2B,MAAkC;EAC3D,MAAM,cAAc,KAAK,aAAa;EACtC,MAAM,WAAW,IAAI,IAAI,KAAK,aAAa,IAAI;EAE/C,KAAK,aAAa,UAAU,KAAK,WAAW;EAC5C,KAAK,aAAa,OAAO,IAAI,IAAI,KAAK,IAAI;EAG1C,MAAM,cACJ,SAAS,SAAS,KAAK,aAAa,KAAK,QACzC,CAAC,GAAG,KAAK,aAAa,IAAI,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;EAE5D,IAAI,gBAAgB,KAAK,aAAa,WAAW,aAC/C,KAAK,WAAW;CAEpB;CAEA,sBAA8B;EAC5B,KAAK,cAAc,cAAc,gBAAgB;GAC/C,KAAK,WAAW;EAClB,CAAC;CACH;CAEA,aAAqB;EACnB,MAAM,SAA6B;GACjC,MAAM;GACN,MAAM,CAAC;EACT;EAEA,MAAM,UAAU,cAAc,QAAQ;EACtC,KAAK,MAAM,qBAAqB,KAAK,aAAa,MAChD,IAAI,CAAC,QAAQ,IAAI,iBAAiB,GAChC,KAAK,aAAa,KAAK,OAAO,iBAAiB;EAInD,IAAI,KAAK,aAAa,SAAS;GAC7B,OAAO,KAAK,UAAU,CAAC,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,MAAM,EAAE;GAEpE,IAAI,KAAK,aAAa,KAAK,SAAS,KAAK,QAAQ,OAAO,GACtD,KAAK,aAAa,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAM,CAAC;EAEnE;EAEA,IAAI,KAAK,aAAa,KAAK,OAAO,GAAG;GACnC,OAAO,KAAK,OAAO,CAAC;GAEpB,KAAK,MAAM,SAAS,KAAK,aAAa,MAAM;IAC1C,MAAM,WAAW,QAAQ,IAAI,KAAK;IAClC,IAAI,UAAU;KAGZ,MAAM,QAAiC,CAAC;KACxC,MAAM,SAKD,CAAC;KACN,IAAI,SAAS;WACN,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,GAAG,GACrD,IAAI,OAAO,UAAU,cAAc,YAAY,OAAO;OACpD,IAAI,UAAoB,CAAC;OACzB,IAAI;QACF,MAAM,aAAa,MAAM;QACzB,IAAI,cAAc,OAAO,eAAe,UACtC,UAAU,OAAO,KAAK,UAAU,CAAC,CAAC,QAC/B,QACC,OAAQ,WAAuC,SAC/C,UACJ;QAEF,IAAI,MAAM,WAAW,QACnB,MAAM,QAAQ,YAAY,WAAW,KAAK;OAE9C,QAAQ,CAGR;OACA,OAAO,KAAK;QACV;QACA,QAAQ,MAAM;QACd,OAAO,MAAM;QACb;OACF,CAAC;MACH;;KAKJ,MAAM,eAAe,sBACnB,SAAS,KAAK,SAAS,CAAC,CAAC,gBAAgB,CAC3C;KAEA,OAAO,KAAK,KAAK,KAAK;MACpB;MACA,OAAO,mBAAmB,KAAK;MAC/B,QAAQ,mBAAmB,SAAS,IAAI;MAC1B;MACd,QAAQ,kBAAkB,MAAM;KAClC,CAAC;IACH;GACF;EACF;EAEA,IAAI,OAAO,KAAK,OAAO,IAAI,CAAC,CAAC,WAAW,GACtC;EAGF,KAAK,cAAc,MAAM;CAC3B;CAEA,UAAU;EACR,IAAI,KAAK,aACP,KAAK,YAAY;CAErB;AACF"}
|
package/dist/DevToolsModal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/DevToolsModal.d.ts
|
|
2
|
-
declare const DevToolsModal: () => import("react
|
|
2
|
+
declare const DevToolsModal: () => import("react").JSX.Element | null;
|
|
3
3
|
//#endregion
|
|
4
4
|
export { DevToolsModal };
|
|
5
5
|
//# sourceMappingURL=DevToolsModal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevToolsModal.d.ts","names":[],"sources":["../src/DevToolsModal.tsx"],"mappings":";
|
|
1
|
+
{"version":3,"file":"DevToolsModal.d.ts","names":[],"sources":["../src/DevToolsModal.tsx"],"mappings":";cAiLa,aAAA,wBAAa,GAAA,CAAA,OAAA"}
|
package/dist/DevToolsModal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { DevToolsFrame } from "./DevToolsFrame.js";
|
|
3
3
|
import { ANIMATION_STYLES, getStyles } from "./styles/DevToolsModal.styles.js";
|
|
4
|
-
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from "@assistant-ui/tap/react-shim";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
//#region src/DevToolsModal.tsx
|
|
7
7
|
const isDarkMode = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevToolsModal.js","names":[],"sources":["../src/DevToolsModal.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { DevToolsFrame } from \"./DevToolsFrame\";\nimport { getStyles, ANIMATION_STYLES } from \"./styles/DevToolsModal.styles\";\n\nconst isDarkMode = (): boolean => {\n if (typeof document === \"undefined\") return false;\n return (\n document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n );\n};\n\nconst subscribeToThemeChanges = (callback: () => void) => {\n if (typeof MutationObserver === \"undefined\") {\n return () => {};\n }\n\n const observer = new MutationObserver(callback);\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n\n if (document.body !== document.documentElement) {\n observer.observe(document.body, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n }\n\n return () => observer.disconnect();\n};\n\nconst DevToolsModalImpl = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [buttonHover, setButtonHover] = useState(false);\n const [closeHover, setCloseHover] = useState(false);\n\n const darkMode = useSyncExternalStore(\n subscribeToThemeChanges,\n isDarkMode,\n () => false, // Server-side always returns false\n );\n\n const styles = useMemo(() => getStyles(darkMode), [darkMode]);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n\n const styleId = \"devtools-modal-animations\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = ANIMATION_STYLES;\n document.head.appendChild(style);\n }\n\n return () => {\n const style = document.getElementById(styleId);\n if (style && !document.querySelector(\"[data-devtools-modal]\")) {\n style.remove();\n }\n };\n }, []);\n\n useEffect(() => {\n if (!isOpen) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n setIsOpen(false);\n }\n };\n\n document.addEventListener(\"keydown\", handleEscape);\n return () => document.removeEventListener(\"keydown\", handleEscape);\n }, [isOpen]);\n\n return (\n <>\n <div style={styles.floatingContainer}>\n <button\n type=\"button\"\n onClick={() => setIsOpen(true)}\n onMouseEnter={() => setButtonHover(true)}\n onMouseLeave={() => setButtonHover(false)}\n style={{\n ...styles.floatingButton,\n ...(buttonHover ? styles.floatingButtonHover : {}),\n }}\n aria-label=\"Open assistant-ui DevTools\"\n title=\"Open assistant-ui DevTools\"\n >\n <svg\n aria-hidden=\"true\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{ width: \"20px\", height: \"20px\" }}\n >\n <path\n d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n fill=\"none\"\n />\n </svg>\n </button>\n </div>\n\n {isOpen && (\n <>\n
|
|
1
|
+
{"version":3,"file":"DevToolsModal.js","names":[],"sources":["../src/DevToolsModal.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { DevToolsFrame } from \"./DevToolsFrame\";\nimport { getStyles, ANIMATION_STYLES } from \"./styles/DevToolsModal.styles\";\n\nconst isDarkMode = (): boolean => {\n if (typeof document === \"undefined\") return false;\n return (\n document.documentElement.classList.contains(\"dark\") ||\n document.body.classList.contains(\"dark\")\n );\n};\n\nconst subscribeToThemeChanges = (callback: () => void) => {\n if (typeof MutationObserver === \"undefined\") {\n return () => {};\n }\n\n const observer = new MutationObserver(callback);\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n\n if (document.body !== document.documentElement) {\n observer.observe(document.body, {\n attributes: true,\n attributeFilter: [\"class\"],\n });\n }\n\n return () => observer.disconnect();\n};\n\nconst DevToolsModalImpl = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [buttonHover, setButtonHover] = useState(false);\n const [closeHover, setCloseHover] = useState(false);\n\n const darkMode = useSyncExternalStore(\n subscribeToThemeChanges,\n isDarkMode,\n () => false, // Server-side always returns false\n );\n\n const styles = useMemo(() => getStyles(darkMode), [darkMode]);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n\n const styleId = \"devtools-modal-animations\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = ANIMATION_STYLES;\n document.head.appendChild(style);\n }\n\n return () => {\n const style = document.getElementById(styleId);\n if (style && !document.querySelector(\"[data-devtools-modal]\")) {\n style.remove();\n }\n };\n }, []);\n\n useEffect(() => {\n if (!isOpen) return;\n\n const handleEscape = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n setIsOpen(false);\n }\n };\n\n document.addEventListener(\"keydown\", handleEscape);\n return () => document.removeEventListener(\"keydown\", handleEscape);\n }, [isOpen]);\n\n return (\n <>\n <div style={styles.floatingContainer}>\n <button\n type=\"button\"\n onClick={() => setIsOpen(true)}\n onMouseEnter={() => setButtonHover(true)}\n onMouseLeave={() => setButtonHover(false)}\n style={{\n ...styles.floatingButton,\n ...(buttonHover ? styles.floatingButtonHover : {}),\n }}\n aria-label=\"Open assistant-ui DevTools\"\n title=\"Open assistant-ui DevTools\"\n >\n <svg\n aria-hidden=\"true\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n style={{ width: \"20px\", height: \"20px\" }}\n >\n <path\n d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"\n stroke=\"currentColor\"\n strokeWidth=\"1.8\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n fill=\"none\"\n />\n </svg>\n </button>\n </div>\n\n {isOpen && (\n <>\n <div style={styles.backdrop} onClick={() => setIsOpen(false)} />\n\n <div style={styles.modal} data-devtools-modal>\n <button\n type=\"button\"\n onClick={() => setIsOpen(false)}\n onMouseEnter={() => setCloseHover(true)}\n onMouseLeave={() => setCloseHover(false)}\n style={{\n ...styles.dismissButton,\n ...(closeHover ? styles.dismissButtonHover : {}),\n }}\n aria-label=\"Close DevTools\"\n >\n <svg\n aria-hidden=\"true\"\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18 6L6 18\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M6 6L18 18\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n <div style={styles.modalContent}>\n <DevToolsFrame\n style={{\n width: \"100%\",\n height: \"100%\",\n border: \"none\",\n borderRadius: \"12px\",\n backgroundColor: \"transparent\",\n }}\n />\n </div>\n </div>\n </>\n )}\n </>\n );\n};\n\n// Export a component that only renders in development\nexport const DevToolsModal = () => {\n // Check if we're in production - most bundlers will replace process.env.NODE_ENV\n // This allows the entire component to be eliminated via dead code elimination\n if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"production\"\n ) {\n return null;\n }\n\n return <DevToolsModalImpl />;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExtensionHost.d.ts","names":[],"sources":["../src/ExtensionHost.ts"],"mappings":";cAEa,aAAA;EAAA,QACH,YAAA;EAAA,QACA,eAAA;;EAgDR,OAAA
|
|
1
|
+
{"version":3,"file":"ExtensionHost.d.ts","names":[],"sources":["../src/ExtensionHost.ts"],"mappings":";cAEa,aAAA;EAAA,QACH,YAAA;EAAA,QACA,eAAA;;EAgDR,OAAA;AAAA"}
|
package/dist/FrameClient.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameClient.d.ts","names":[],"sources":["../src/FrameClient.ts"],"mappings":";UAAU,OAAA;EACR,KAAA;EACA,KAAA;EACA,MAAA;EACA,
|
|
1
|
+
{"version":3,"file":"FrameClient.d.ts","names":[],"sources":["../src/FrameClient.ts"],"mappings":";UAAU,OAAA;EACR,KAAA;EACA,KAAA;EACA,MAAA;EACA,YAAA;EACA,MAAA;AAAA;AAAA,KAeG,cAAA,IAAkB,IAAA;EACrB,OAAA,GAAU,KAAA;IAAQ,KAAA;EAAA;EAClB,IAAA,GAAO,OAAO;AAAA;AAAA,cAGH,WAAA;EAAA,QACH,SAAA;EAAA,QACA,mBAAA;EAAA,QACA,UAAA;;UASA,oBAAA;EAcR,eAAA,CAAgB,QAAA;EAOhB,SAAA,CAAU,QAAA,EAAU,cAAA;EAapB,eAAA,CAAgB,OAAA;IAAW,OAAA;IAAmB,IAAA;EAAA;EAU9C,WAAA,CAAY,KAAA;EAAA,QAUJ,eAAA;EAIR,aAAA;cAlEY,KAAA;MAAQ,KAAA;IAAA;WACX,OAAA;EAAA;AAAA"}
|
package/dist/FrameClient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameClient.js","names":[],"sources":["../src/FrameClient.ts"],"sourcesContent":["interface ApiData {\n apiId: number;\n state: any;\n events: any[];\n
|
|
1
|
+
{"version":3,"file":"FrameClient.js","names":[],"sources":["../src/FrameClient.ts"],"sourcesContent":["interface ApiData {\n apiId: number;\n state: any;\n events: any[];\n modelContext?: any;\n scopes?: any;\n}\n\ninterface UpdateMessage {\n type: \"update\";\n data: {\n apiList?: Array<{ apiId: number }>;\n apis?: ApiData[];\n };\n}\n\ninterface HostConnectedMessage {\n type: \"host-connected\";\n}\n\ntype UpdateListener = (data: {\n apiList?: Array<{ apiId: number }>;\n apis?: ApiData[];\n}) => void;\n\nexport class FrameClient {\n private listeners = new Set<UpdateListener>();\n private connectionListeners = new Set<() => void>();\n private lastUpdate: {\n apiList?: Array<{ apiId: number }>;\n apis?: ApiData[];\n } = {};\n\n constructor() {\n this.setupMessageListener();\n }\n\n private setupMessageListener() {\n window.addEventListener(\"message\", (event) => {\n const message = event.data as UpdateMessage | HostConnectedMessage;\n\n if (message.type === \"update\") {\n this.lastUpdate = message.data;\n this.notifyListeners(message.data);\n } else if (message.type === \"host-connected\") {\n // Host has reconnected (page refresh), notify listeners to re-subscribe\n this.connectionListeners.forEach((listener) => listener());\n }\n });\n }\n\n onHostConnected(listener: () => void): () => void {\n this.connectionListeners.add(listener);\n return () => {\n this.connectionListeners.delete(listener);\n };\n }\n\n subscribe(listener: UpdateListener): () => void {\n this.listeners.add(listener);\n\n // Send the last update to the new listener\n if (this.lastUpdate.apiList || this.lastUpdate.apis) {\n listener(this.lastUpdate);\n }\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n setSubscription(options: { apiList?: boolean; apis?: number[] }) {\n window.parent.postMessage(\n {\n type: \"subscription\",\n data: options,\n },\n \"*\",\n );\n }\n\n clearEvents(apiId: number) {\n window.parent.postMessage(\n {\n type: \"clearEvents\",\n data: { apiId },\n },\n \"*\",\n );\n }\n\n private notifyListeners(data: UpdateMessage[\"data\"]) {\n this.listeners.forEach((listener) => listener(data));\n }\n\n getLastUpdate() {\n return this.lastUpdate;\n }\n}\n"],"mappings":";AAyBA,IAAa,cAAb,MAAyB;CACvB,4BAAoB,IAAI,IAAoB;CAC5C,sCAA8B,IAAI,IAAgB;CAClD,aAGI,CAAC;CAEL,cAAc;EACZ,KAAK,qBAAqB;CAC5B;CAEA,uBAA+B;EAC7B,OAAO,iBAAiB,YAAY,UAAU;GAC5C,MAAM,UAAU,MAAM;GAEtB,IAAI,QAAQ,SAAS,UAAU;IAC7B,KAAK,aAAa,QAAQ;IAC1B,KAAK,gBAAgB,QAAQ,IAAI;GACnC,OAAO,IAAI,QAAQ,SAAS,kBAE1B,KAAK,oBAAoB,SAAS,aAAa,SAAS,CAAC;EAE7D,CAAC;CACH;CAEA,gBAAgB,UAAkC;EAChD,KAAK,oBAAoB,IAAI,QAAQ;EACrC,aAAa;GACX,KAAK,oBAAoB,OAAO,QAAQ;EAC1C;CACF;CAEA,UAAU,UAAsC;EAC9C,KAAK,UAAU,IAAI,QAAQ;EAG3B,IAAI,KAAK,WAAW,WAAW,KAAK,WAAW,MAC7C,SAAS,KAAK,UAAU;EAG1B,aAAa;GACX,KAAK,UAAU,OAAO,QAAQ;EAChC;CACF;CAEA,gBAAgB,SAAiD;EAC/D,OAAO,OAAO,YACZ;GACE,MAAM;GACN,MAAM;EACR,GACA,GACF;CACF;CAEA,YAAY,OAAe;EACzB,OAAO,OAAO,YACZ;GACE,MAAM;GACN,MAAM,EAAE,MAAM;EAChB,GACA,GACF;CACF;CAEA,gBAAwB,MAA6B;EACnD,KAAK,UAAU,SAAS,aAAa,SAAS,IAAI,CAAC;CACrD;CAEA,gBAAgB;EACd,OAAO,KAAK;CACd;AACF"}
|
package/dist/FrameHost.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameHost.d.ts","names":[],"sources":["../src/FrameHost.ts"],"mappings":";cAEa,SAAA;EAAA,QACH,KAAA;EAAA,QACA,YAAA;EAAA,QACA,eAAA;cAEI,KAAA,EAAO,iBAAiB;EAmBpC,OAAA
|
|
1
|
+
{"version":3,"file":"FrameHost.d.ts","names":[],"sources":["../src/FrameHost.ts"],"mappings":";cAEa,SAAA;EAAA,QACH,KAAA;EAAA,QACA,YAAA;EAAA,QACA,eAAA;cAEI,KAAA,EAAO,iBAAiB;EAmBpC,OAAA;AAAA"}
|
|
@@ -3,7 +3,17 @@ import { ModelContext } from "@assistant-ui/react";
|
|
|
3
3
|
|
|
4
4
|
//#region src/utils/serialization.d.ts
|
|
5
5
|
declare const sanitizeForMessage: (value: unknown, seen?: WeakSet<object>) => unknown;
|
|
6
|
+
declare const REDACTED = "[redacted]";
|
|
7
|
+
/**
|
|
8
|
+
* Mask values whose key names a known credential, and mask every value inside
|
|
9
|
+
* an `env`/`headers` subtree wholesale. Operates on already sanitized plain
|
|
10
|
+
* data (primitives, arrays, plain objects). Applied only to config-bearing
|
|
11
|
+
* subtrees, never to a tool's parameters schema, so a schema property literally
|
|
12
|
+
* named `token` is not corrupted.
|
|
13
|
+
*/
|
|
14
|
+
declare const redactSensitive: (value: unknown, maskAll?: boolean) => unknown;
|
|
15
|
+
declare const sanitizeAndRedact: (value: unknown) => unknown;
|
|
6
16
|
declare const serializeModelContext: (context: ModelContext | undefined) => SerializedModelContext | undefined;
|
|
7
17
|
//#endregion
|
|
8
|
-
export { sanitizeForMessage, serializeModelContext };
|
|
18
|
+
export { REDACTED, redactSensitive, sanitizeAndRedact, sanitizeForMessage, serializeModelContext };
|
|
9
19
|
//# sourceMappingURL=serialization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;;cAIa,kBAAA,GACX,KAAA,WACA,IAAA,GAAA,OAAA;AAAA,cAgDW,qBAAA,GACX,OAAA,EAAS,YAAA,iBACR,sBAAsB"}
|
|
1
|
+
{"version":3,"file":"serialization.d.ts","names":[],"sources":["../../src/utils/serialization.ts"],"mappings":";;;;cAIa,kBAAA,GACX,KAAA,WACA,IAAA,GAAA,OAAA;AAAA,cAgDW,QAAA;AAlDb;;;;;;;AAAA,cA0Fa,eAAA,GAAmB,KAAA,WAAgB,OAAe;AAAA,cAoBlD,iBAAA,GAAqB,KAAc;AAAA,cAGnC,qBAAA,GACX,OAAA,EAAS,YAAA,iBACR,sBAAsB"}
|
|
@@ -25,6 +25,55 @@ const sanitizeForMessage = (value, seen = /* @__PURE__ */ new WeakSet()) => {
|
|
|
25
25
|
}
|
|
26
26
|
return value;
|
|
27
27
|
};
|
|
28
|
+
const REDACTED = "[redacted]";
|
|
29
|
+
const SENSITIVE_KEYS = new Set([
|
|
30
|
+
"apikey",
|
|
31
|
+
"xapikey",
|
|
32
|
+
"accesskey",
|
|
33
|
+
"authorization",
|
|
34
|
+
"password",
|
|
35
|
+
"passwd",
|
|
36
|
+
"secret",
|
|
37
|
+
"clientsecret",
|
|
38
|
+
"token",
|
|
39
|
+
"accesstoken",
|
|
40
|
+
"refreshtoken",
|
|
41
|
+
"cookie",
|
|
42
|
+
"setcookie",
|
|
43
|
+
"credential",
|
|
44
|
+
"credentials",
|
|
45
|
+
"privatekey",
|
|
46
|
+
"bearer",
|
|
47
|
+
"sessionid"
|
|
48
|
+
]);
|
|
49
|
+
const normalizeKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
|
|
50
|
+
/**
|
|
51
|
+
* Subtrees that are credential maps with arbitrary, user-defined key names
|
|
52
|
+
* (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch
|
|
53
|
+
* `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these
|
|
54
|
+
* is masked wholesale.
|
|
55
|
+
*/
|
|
56
|
+
const MASK_ALL_KEYS = new Set(["env", "headers"]);
|
|
57
|
+
/**
|
|
58
|
+
* Mask values whose key names a known credential, and mask every value inside
|
|
59
|
+
* an `env`/`headers` subtree wholesale. Operates on already sanitized plain
|
|
60
|
+
* data (primitives, arrays, plain objects). Applied only to config-bearing
|
|
61
|
+
* subtrees, never to a tool's parameters schema, so a schema property literally
|
|
62
|
+
* named `token` is not corrupted.
|
|
63
|
+
*/
|
|
64
|
+
const redactSensitive = (value, maskAll = false) => {
|
|
65
|
+
if (Array.isArray(value)) return value.map((entry) => redactSensitive(entry, maskAll));
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
69
|
+
const normalized = normalizeKey(key);
|
|
70
|
+
result[key] = maskAll || SENSITIVE_KEYS.has(normalized) ? REDACTED : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
return maskAll ? REDACTED : value;
|
|
75
|
+
};
|
|
76
|
+
const sanitizeAndRedact = (value) => redactSensitive(sanitizeForMessage(value));
|
|
28
77
|
const serializeModelContext = (context) => {
|
|
29
78
|
if (!context || typeof context !== "object") return;
|
|
30
79
|
const modelContext = context;
|
|
@@ -32,21 +81,27 @@ const serializeModelContext = (context) => {
|
|
|
32
81
|
const systemValue = modelContext.system;
|
|
33
82
|
if (typeof systemValue === "string" && systemValue.length > 0) result.system = systemValue;
|
|
34
83
|
const tools = normalizeToolList(modelContext.tools);
|
|
35
|
-
if (tools.length > 0) result.tools = tools.map((tool) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
84
|
+
if (tools.length > 0) result.tools = tools.map((tool) => {
|
|
85
|
+
return {
|
|
86
|
+
...tool,
|
|
87
|
+
parameters: sanitizeForMessage(tool.parameters),
|
|
88
|
+
...tool.providerOptions !== void 0 ? { providerOptions: sanitizeAndRedact(tool.providerOptions) } : {},
|
|
89
|
+
...tool.providerArgs !== void 0 ? { providerArgs: sanitizeAndRedact(tool.providerArgs) } : {},
|
|
90
|
+
...tool.server !== void 0 ? { server: sanitizeAndRedact(tool.server) } : {},
|
|
91
|
+
...tool.backendDefault !== void 0 ? { backendDefault: sanitizeForMessage(tool.backendDefault) } : {}
|
|
92
|
+
};
|
|
93
|
+
});
|
|
39
94
|
if (modelContext.callSettings !== void 0) {
|
|
40
|
-
const callSettings =
|
|
95
|
+
const callSettings = sanitizeAndRedact(modelContext.callSettings);
|
|
41
96
|
if (callSettings && typeof callSettings === "object" && !Array.isArray(callSettings)) result.callSettings = callSettings;
|
|
42
97
|
}
|
|
43
98
|
if (modelContext.config !== void 0) {
|
|
44
|
-
const config =
|
|
99
|
+
const config = sanitizeAndRedact(modelContext.config);
|
|
45
100
|
if (config && typeof config === "object" && !Array.isArray(config)) result.config = config;
|
|
46
101
|
}
|
|
47
102
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
48
103
|
};
|
|
49
104
|
//#endregion
|
|
50
|
-
export { sanitizeForMessage, serializeModelContext };
|
|
105
|
+
export { REDACTED, redactSensitive, sanitizeAndRedact, sanitizeForMessage, serializeModelContext };
|
|
51
106
|
|
|
52
107
|
//# sourceMappingURL=serialization.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.js","names":[],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList } from \"./toolNormalization\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n if (value instanceof Map) {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of value.entries()) {\n result[String(key)] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n if (value instanceof Set) {\n return Array.from(value).map((entry) => sanitizeForMessage(entry, seen));\n }\n if (Array.isArray(value)) {\n if (seen.has(value as unknown as object)) return \"[Circular]\";\n seen.add(value as unknown as object);\n return value\n .map((entry) => sanitizeForMessage(entry, seen))\n .filter((item) => item !== undefined);\n }\n if (typeof value === \"object\") {\n if (seen.has(value as object)) return \"[Circular]\";\n seen.add(value as object);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n result[key] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n return value;\n};\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = modelContext.system;\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(modelContext.tools);\n if (tools.length > 0) {\n result.tools = tools.map((tool) =>
|
|
1
|
+
{"version":3,"file":"serialization.js","names":[],"sources":["../../src/utils/serialization.ts"],"sourcesContent":["import type { ModelContext } from \"@assistant-ui/react\";\nimport type { SerializedModelContext } from \"../types\";\nimport { normalizeToolList, type NormalizedTool } from \"./toolNormalization\";\n\nexport const sanitizeForMessage = (\n value: unknown,\n seen = new WeakSet<object>(),\n): unknown => {\n // Early return for primitives\n if (value === null || value === undefined) return value;\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"function\") {\n return \"[Function]\";\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n if (value instanceof Map) {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of value.entries()) {\n result[String(key)] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n if (value instanceof Set) {\n return Array.from(value).map((entry) => sanitizeForMessage(entry, seen));\n }\n if (Array.isArray(value)) {\n if (seen.has(value as unknown as object)) return \"[Circular]\";\n seen.add(value as unknown as object);\n return value\n .map((entry) => sanitizeForMessage(entry, seen))\n .filter((item) => item !== undefined);\n }\n if (typeof value === \"object\") {\n if (seen.has(value as object)) return \"[Circular]\";\n seen.add(value as object);\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n result[key] = sanitizeForMessage(entry, seen);\n }\n return result;\n }\n return value;\n};\n\nexport const REDACTED = \"[redacted]\";\n\nconst SENSITIVE_KEYS = new Set([\n \"apikey\",\n \"xapikey\",\n \"accesskey\",\n \"authorization\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"clientsecret\",\n \"token\",\n \"accesstoken\",\n \"refreshtoken\",\n \"cookie\",\n \"setcookie\",\n \"credential\",\n \"credentials\",\n \"privatekey\",\n \"bearer\",\n \"sessionid\",\n]);\n\nconst normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, \"\");\n\n/**\n * Subtrees that are credential maps with arbitrary, user-defined key names\n * (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch\n * `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these\n * is masked wholesale.\n */\nconst MASK_ALL_KEYS = new Set([\"env\", \"headers\"]);\n\n/**\n * Mask values whose key names a known credential, and mask every value inside\n * an `env`/`headers` subtree wholesale. Operates on already sanitized plain\n * data (primitives, arrays, plain objects). Applied only to config-bearing\n * subtrees, never to a tool's parameters schema, so a schema property literally\n * named `token` is not corrupted.\n */\nexport const redactSensitive = (value: unknown, maskAll = false): unknown => {\n if (Array.isArray(value)) {\n return value.map((entry) => redactSensitive(entry, maskAll));\n }\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(\n value as Record<string, unknown>,\n )) {\n const normalized = normalizeKey(key);\n result[key] =\n maskAll || SENSITIVE_KEYS.has(normalized)\n ? REDACTED\n : redactSensitive(entry, MASK_ALL_KEYS.has(normalized));\n }\n return result;\n }\n return maskAll ? REDACTED : value;\n};\n\nexport const sanitizeAndRedact = (value: unknown): unknown =>\n redactSensitive(sanitizeForMessage(value));\n\nexport const serializeModelContext = (\n context: ModelContext | undefined,\n): SerializedModelContext | undefined => {\n if (!context || typeof context !== \"object\") {\n return undefined;\n }\n\n const modelContext = context as Record<string, unknown>;\n const result: SerializedModelContext = {};\n\n const systemValue = modelContext.system;\n if (typeof systemValue === \"string\" && systemValue.length > 0) {\n result.system = systemValue;\n }\n\n const tools = normalizeToolList(modelContext.tools);\n if (tools.length > 0) {\n result.tools = tools.map((tool): NormalizedTool => {\n return {\n ...tool,\n parameters: sanitizeForMessage(tool.parameters),\n ...(tool.providerOptions !== undefined\n ? { providerOptions: sanitizeAndRedact(tool.providerOptions) }\n : {}),\n ...(tool.providerArgs !== undefined\n ? { providerArgs: sanitizeAndRedact(tool.providerArgs) }\n : {}),\n ...(tool.server !== undefined\n ? { server: sanitizeAndRedact(tool.server) }\n : {}),\n ...(tool.backendDefault !== undefined\n ? { backendDefault: sanitizeForMessage(tool.backendDefault) }\n : {}),\n };\n });\n }\n\n if (modelContext.callSettings !== undefined) {\n const callSettings = sanitizeAndRedact(modelContext.callSettings);\n if (\n callSettings &&\n typeof callSettings === \"object\" &&\n !Array.isArray(callSettings)\n ) {\n result.callSettings = callSettings as Record<string, unknown>;\n }\n }\n\n if (modelContext.config !== undefined) {\n const config = sanitizeAndRedact(modelContext.config);\n if (config && typeof config === \"object\" && !Array.isArray(config)) {\n result.config = config as Record<string, unknown>;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n};\n"],"mappings":"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"}
|
|
@@ -4,6 +4,13 @@ type NormalizedTool = {
|
|
|
4
4
|
type?: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
+
display?: string;
|
|
8
|
+
providerId?: string;
|
|
9
|
+
supportsDeferredResults?: boolean;
|
|
10
|
+
backendDefault?: unknown;
|
|
11
|
+
providerOptions?: unknown;
|
|
12
|
+
providerArgs?: unknown;
|
|
13
|
+
server?: unknown;
|
|
7
14
|
parameters?: unknown;
|
|
8
15
|
};
|
|
9
16
|
declare const normalizeToolList: (value: unknown) => NormalizedTool[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolNormalization.d.ts","names":[],"sources":["../../src/utils/toolNormalization.ts"],"mappings":";KAEY,cAAA;EACV,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,UAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"toolNormalization.d.ts","names":[],"sources":["../../src/utils/toolNormalization.ts"],"mappings":";KAEY,cAAA;EACV,IAAA;EACA,IAAA;EACA,WAAA;EACA,QAAA;EACA,OAAA;EACA,UAAA;EACA,uBAAA;EACA,cAAA;EACA,eAAA;EACA,YAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,cAuEW,iBAAA,GAAqB,KAAA,cAAiB,cAAc"}
|
|
@@ -14,6 +14,13 @@ const mapToNormalizedTool = (name, raw) => {
|
|
|
14
14
|
if (typeof raw.type === "string") tool.type = raw.type;
|
|
15
15
|
if (typeof raw.description === "string") tool.description = raw.description;
|
|
16
16
|
if (typeof raw.disabled === "boolean") tool.disabled = raw.disabled;
|
|
17
|
+
if (typeof raw.display === "string") tool.display = raw.display;
|
|
18
|
+
if (typeof raw.providerId === "string") tool.providerId = raw.providerId;
|
|
19
|
+
if (typeof raw.supportsDeferredResults === "boolean") tool.supportsDeferredResults = raw.supportsDeferredResults;
|
|
20
|
+
if (raw.unstable_backendDefault !== void 0) tool.backendDefault = raw.unstable_backendDefault;
|
|
21
|
+
if (raw.providerOptions !== void 0) tool.providerOptions = raw.providerOptions;
|
|
22
|
+
if (raw.args !== void 0) tool.providerArgs = raw.args;
|
|
23
|
+
if (raw.server !== void 0) tool.server = raw.server;
|
|
17
24
|
if (Object.hasOwn(raw, "parameters")) tool.parameters = toJsonSchema(raw.parameters);
|
|
18
25
|
return tool;
|
|
19
26
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolNormalization.js","names":[],"sources":["../../src/utils/toolNormalization.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport type NormalizedTool = {\n name: string;\n type?: string;\n description?: string;\n disabled?: boolean;\n parameters?: unknown;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n\nconst toJsonSchema = (value: unknown): unknown => {\n if (value instanceof z.ZodType) {\n try {\n return z.toJSONSchema(value);\n } catch {\n return value;\n }\n }\n\n return value;\n};\n\nconst mapToNormalizedTool = (\n name: string,\n raw: Record<string, unknown>,\n): NormalizedTool => {\n const tool: NormalizedTool = { name };\n\n if (typeof raw.type === \"string\") {\n tool.type = raw.type as string;\n }\n\n if (typeof raw.description === \"string\") {\n tool.description = raw.description as string;\n }\n\n if (typeof raw.disabled === \"boolean\") {\n tool.disabled = raw.disabled as boolean;\n }\n\n if (Object.hasOwn(raw, \"parameters\")) {\n tool.parameters = toJsonSchema(raw.parameters);\n }\n\n return tool;\n};\n\nexport const normalizeToolList = (value: unknown): NormalizedTool[] => {\n if (!value || typeof value !== \"object\") {\n return [];\n }\n\n if (Array.isArray(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const entry of value) {\n if (!isRecord(entry) || typeof entry.name !== \"string\") continue;\n tools.push(mapToNormalizedTool(entry.name as string, entry));\n }\n\n return tools;\n }\n\n if (isRecord(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const [name, entry] of Object.entries(value)) {\n if (!isRecord(entry)) {\n tools.push({ name });\n continue;\n }\n\n tools.push(mapToNormalizedTool(name, entry));\n }\n\n return tools;\n }\n\n return [];\n};\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"toolNormalization.js","names":[],"sources":["../../src/utils/toolNormalization.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport type NormalizedTool = {\n name: string;\n type?: string;\n description?: string;\n disabled?: boolean;\n display?: string;\n providerId?: string;\n supportsDeferredResults?: boolean;\n backendDefault?: unknown;\n providerOptions?: unknown;\n providerArgs?: unknown;\n server?: unknown;\n parameters?: unknown;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\";\n\nconst toJsonSchema = (value: unknown): unknown => {\n if (value instanceof z.ZodType) {\n try {\n return z.toJSONSchema(value);\n } catch {\n return value;\n }\n }\n\n return value;\n};\n\nconst mapToNormalizedTool = (\n name: string,\n raw: Record<string, unknown>,\n): NormalizedTool => {\n const tool: NormalizedTool = { name };\n\n if (typeof raw.type === \"string\") {\n tool.type = raw.type as string;\n }\n\n if (typeof raw.description === \"string\") {\n tool.description = raw.description as string;\n }\n\n if (typeof raw.disabled === \"boolean\") {\n tool.disabled = raw.disabled as boolean;\n }\n\n if (typeof raw.display === \"string\") {\n tool.display = raw.display as string;\n }\n\n if (typeof raw.providerId === \"string\") {\n tool.providerId = raw.providerId as string;\n }\n\n if (typeof raw.supportsDeferredResults === \"boolean\") {\n tool.supportsDeferredResults = raw.supportsDeferredResults as boolean;\n }\n\n if (raw.unstable_backendDefault !== undefined) {\n tool.backendDefault = raw.unstable_backendDefault;\n }\n\n if (raw.providerOptions !== undefined) {\n tool.providerOptions = raw.providerOptions;\n }\n\n if (raw.args !== undefined) {\n tool.providerArgs = raw.args;\n }\n\n if (raw.server !== undefined) {\n tool.server = raw.server;\n }\n\n if (Object.hasOwn(raw, \"parameters\")) {\n tool.parameters = toJsonSchema(raw.parameters);\n }\n\n return tool;\n};\n\nexport const normalizeToolList = (value: unknown): NormalizedTool[] => {\n if (!value || typeof value !== \"object\") {\n return [];\n }\n\n if (Array.isArray(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const entry of value) {\n if (!isRecord(entry) || typeof entry.name !== \"string\") continue;\n tools.push(mapToNormalizedTool(entry.name as string, entry));\n }\n\n return tools;\n }\n\n if (isRecord(value)) {\n const tools: NormalizedTool[] = [];\n\n for (const [name, entry] of Object.entries(value)) {\n if (!isRecord(entry)) {\n tools.push({ name });\n continue;\n }\n\n tools.push(mapToNormalizedTool(name, entry));\n }\n\n return tools;\n }\n\n return [];\n};\n"],"mappings":";;AAiBA,MAAM,YAAY,UAChB,UAAU,QAAQ,OAAO,UAAU;AAErC,MAAM,gBAAgB,UAA4B;CAChD,IAAI,iBAAiB,EAAE,SACrB,IAAI;EACF,OAAO,EAAE,aAAa,KAAK;CAC7B,QAAQ;EACN,OAAO;CACT;CAGF,OAAO;AACT;AAEA,MAAM,uBACJ,MACA,QACmB;CACnB,MAAM,OAAuB,EAAE,KAAK;CAEpC,IAAI,OAAO,IAAI,SAAS,UACtB,KAAK,OAAO,IAAI;CAGlB,IAAI,OAAO,IAAI,gBAAgB,UAC7B,KAAK,cAAc,IAAI;CAGzB,IAAI,OAAO,IAAI,aAAa,WAC1B,KAAK,WAAW,IAAI;CAGtB,IAAI,OAAO,IAAI,YAAY,UACzB,KAAK,UAAU,IAAI;CAGrB,IAAI,OAAO,IAAI,eAAe,UAC5B,KAAK,aAAa,IAAI;CAGxB,IAAI,OAAO,IAAI,4BAA4B,WACzC,KAAK,0BAA0B,IAAI;CAGrC,IAAI,IAAI,4BAA4B,KAAA,GAClC,KAAK,iBAAiB,IAAI;CAG5B,IAAI,IAAI,oBAAoB,KAAA,GAC1B,KAAK,kBAAkB,IAAI;CAG7B,IAAI,IAAI,SAAS,KAAA,GACf,KAAK,eAAe,IAAI;CAG1B,IAAI,IAAI,WAAW,KAAA,GACjB,KAAK,SAAS,IAAI;CAGpB,IAAI,OAAO,OAAO,KAAK,YAAY,GACjC,KAAK,aAAa,aAAa,IAAI,UAAU;CAG/C,OAAO;AACT;AAEA,MAAa,qBAAqB,UAAqC;CACrE,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO,CAAC;CAGV,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,MAAM,QAA0B,CAAC;EAEjC,KAAK,MAAM,SAAS,OAAO;GACzB,IAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,SAAS,UAAU;GACxD,MAAM,KAAK,oBAAoB,MAAM,MAAgB,KAAK,CAAC;EAC7D;EAEA,OAAO;CACT;CAEA,IAAI,SAAS,KAAK,GAAG;EACnB,MAAM,QAA0B,CAAC;EAEjC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,KAAK,GAAG;GACjD,IAAI,CAAC,SAAS,KAAK,GAAG;IACpB,MAAM,KAAK,EAAE,KAAK,CAAC;IACnB;GACF;GAEA,MAAM,KAAK,oBAAoB,MAAM,KAAK,CAAC;EAC7C;EAEA,OAAO;CACT;CAEA,OAAO,CAAC;AACV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-devtools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "React development tools for assistant-ui components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"assistant-ui",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"zod": "^4.4.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@assistant-ui/react": "^0.14.
|
|
34
|
-
"@assistant-ui/tap": "^0.
|
|
33
|
+
"@assistant-ui/react": "^0.14.14",
|
|
34
|
+
"@assistant-ui/tap": "^0.6.0",
|
|
35
35
|
"@types/react": "*",
|
|
36
36
|
"@types/react-dom": "*",
|
|
37
37
|
"react": "^18 || ^19",
|
|
@@ -46,14 +46,15 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/node": "^25.9.
|
|
50
|
-
"@types/react": "^19.2.
|
|
49
|
+
"@types/node": "^25.9.2",
|
|
50
|
+
"@types/react": "^19.2.17",
|
|
51
51
|
"@types/react-dom": "^19.2.3",
|
|
52
|
-
"react": "^19.2.
|
|
53
|
-
"react-dom": "^19.2.
|
|
54
|
-
"
|
|
55
|
-
"@assistant-ui/
|
|
56
|
-
"@assistant-ui/
|
|
52
|
+
"react": "^19.2.7",
|
|
53
|
+
"react-dom": "^19.2.7",
|
|
54
|
+
"vitest": "^4.1.8",
|
|
55
|
+
"@assistant-ui/react": "0.14.15",
|
|
56
|
+
"@assistant-ui/tap": "0.6.0",
|
|
57
|
+
"@assistant-ui/x-buildutils": "0.0.12"
|
|
57
58
|
},
|
|
58
59
|
"publishConfig": {
|
|
59
60
|
"access": "public",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
70
71
|
},
|
|
71
72
|
"scripts": {
|
|
72
|
-
"build": "aui-build"
|
|
73
|
+
"build": "aui-build",
|
|
74
|
+
"test": "vitest run"
|
|
73
75
|
}
|
|
74
76
|
}
|
package/src/DevToolsHost.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DevToolsHooks } from "@assistant-ui/react";
|
|
2
2
|
import {
|
|
3
|
+
sanitizeAndRedact,
|
|
3
4
|
sanitizeForMessage,
|
|
4
5
|
serializeModelContext,
|
|
5
6
|
} from "./utils/serialization";
|
|
@@ -22,6 +23,7 @@ interface HostToFrameMessage {
|
|
|
22
23
|
state: any;
|
|
23
24
|
events: any[];
|
|
24
25
|
modelContext?: any;
|
|
26
|
+
scopes?: any;
|
|
25
27
|
}>;
|
|
26
28
|
};
|
|
27
29
|
}
|
|
@@ -106,16 +108,41 @@ export class DevToolsHost {
|
|
|
106
108
|
for (const apiId of this.subscription.apis) {
|
|
107
109
|
const apiEntry = allApis.get(apiId);
|
|
108
110
|
if (apiEntry) {
|
|
109
|
-
// Collect state
|
|
111
|
+
// Collect root-scope state plus the identity and method catalog of
|
|
112
|
+
// every accessor (root and derived) for the scope graph.
|
|
110
113
|
const state: Record<string, unknown> = {};
|
|
114
|
+
const scopes: Array<{
|
|
115
|
+
name: string;
|
|
116
|
+
source: string | null;
|
|
117
|
+
query: Record<string, unknown> | null;
|
|
118
|
+
methods: string[];
|
|
119
|
+
}> = [];
|
|
111
120
|
if (apiEntry.api) {
|
|
112
121
|
for (const [name, scope] of Object.entries(apiEntry.api)) {
|
|
113
122
|
if (typeof scope === "function" && "source" in scope) {
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
let methods: string[] = [];
|
|
124
|
+
try {
|
|
116
125
|
const scopeValue = scope();
|
|
117
|
-
|
|
126
|
+
if (scopeValue && typeof scopeValue === "object") {
|
|
127
|
+
methods = Object.keys(scopeValue).filter(
|
|
128
|
+
(key) =>
|
|
129
|
+
typeof (scopeValue as Record<string, unknown>)[key] ===
|
|
130
|
+
"function",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (scope.source === "root") {
|
|
134
|
+
state[name] = scopeValue?.getState?.() ?? scopeValue;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// scope() may throw (derived scopes before the client is
|
|
138
|
+
// mounted, or a broken root scope implementation).
|
|
118
139
|
}
|
|
140
|
+
scopes.push({
|
|
141
|
+
name,
|
|
142
|
+
source: scope.source,
|
|
143
|
+
query: scope.query,
|
|
144
|
+
methods,
|
|
145
|
+
});
|
|
119
146
|
}
|
|
120
147
|
}
|
|
121
148
|
}
|
|
@@ -130,6 +157,7 @@ export class DevToolsHost {
|
|
|
130
157
|
state: sanitizeForMessage(state),
|
|
131
158
|
events: sanitizeForMessage(apiEntry.logs) as unknown[],
|
|
132
159
|
modelContext: modelContext,
|
|
160
|
+
scopes: sanitizeAndRedact(scopes),
|
|
133
161
|
});
|
|
134
162
|
}
|
|
135
163
|
}
|
package/src/DevToolsModal.tsx
CHANGED
|
@@ -117,8 +117,6 @@ const DevToolsModalImpl = () => {
|
|
|
117
117
|
|
|
118
118
|
{isOpen && (
|
|
119
119
|
<>
|
|
120
|
-
{/* biome-ignore lint/a11y/useKeyWithClickEvents: backdrop click is a convenience; keyboard users use the close button */}
|
|
121
|
-
{/* biome-ignore lint/a11y/noStaticElementInteractions: backdrop overlay */}
|
|
122
120
|
<div style={styles.backdrop} onClick={() => setIsOpen(false)} />
|
|
123
121
|
|
|
124
122
|
<div style={styles.modal} data-devtools-modal>
|
package/src/FrameClient.ts
CHANGED
|
@@ -2,7 +2,8 @@ interface ApiData {
|
|
|
2
2
|
apiId: number;
|
|
3
3
|
state: any;
|
|
4
4
|
events: any[];
|
|
5
|
-
|
|
5
|
+
modelContext?: any;
|
|
6
|
+
scopes?: any;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
interface UpdateMessage {
|
|
@@ -43,7 +44,6 @@ export class FrameClient {
|
|
|
43
44
|
this.notifyListeners(message.data);
|
|
44
45
|
} else if (message.type === "host-connected") {
|
|
45
46
|
// Host has reconnected (page refresh), notify listeners to re-subscribe
|
|
46
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
47
47
|
this.connectionListeners.forEach((listener) => listener());
|
|
48
48
|
}
|
|
49
49
|
});
|
|
@@ -90,7 +90,6 @@ export class FrameClient {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
private notifyListeners(data: UpdateMessage["data"]) {
|
|
93
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
94
93
|
this.listeners.forEach((listener) => listener(data));
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
REDACTED,
|
|
4
|
+
redactSensitive,
|
|
5
|
+
serializeModelContext,
|
|
6
|
+
} from "./serialization";
|
|
7
|
+
|
|
8
|
+
describe("redactSensitive", () => {
|
|
9
|
+
it("masks credential-named keys but leaves lookalikes alone", () => {
|
|
10
|
+
const out = redactSensitive({
|
|
11
|
+
apiKey: "sk-123",
|
|
12
|
+
authorization: "Bearer abc",
|
|
13
|
+
maxTokens: 100,
|
|
14
|
+
tokenCount: 42,
|
|
15
|
+
nested: { client_secret: "shh", model: "gpt" },
|
|
16
|
+
}) as Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
expect(out.apiKey).toBe(REDACTED);
|
|
19
|
+
expect(out.authorization).toBe(REDACTED);
|
|
20
|
+
expect(out.maxTokens).toBe(100);
|
|
21
|
+
expect(out.tokenCount).toBe(42);
|
|
22
|
+
expect(out.nested).toEqual({ client_secret: REDACTED, model: "gpt" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("recurses through arrays", () => {
|
|
26
|
+
const out = redactSensitive([{ token: "a" }, { ok: 1 }]) as unknown[];
|
|
27
|
+
expect(out).toEqual([{ token: REDACTED }, { ok: 1 }]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("masks every value inside env/headers, including arbitrary names", () => {
|
|
31
|
+
const out = redactSensitive({
|
|
32
|
+
env: { OPENAI_API_KEY: "sk-1", GITHUB_TOKEN: "ghp", PATH: "/bin" },
|
|
33
|
+
headers: { "X-Custom-Auth": "abc", "Content-Type": "application/json" },
|
|
34
|
+
url: "https://mcp.example.com",
|
|
35
|
+
}) as Record<string, Record<string, unknown>>;
|
|
36
|
+
|
|
37
|
+
expect(out.env).toEqual({
|
|
38
|
+
OPENAI_API_KEY: REDACTED,
|
|
39
|
+
GITHUB_TOKEN: REDACTED,
|
|
40
|
+
PATH: REDACTED,
|
|
41
|
+
});
|
|
42
|
+
expect(out.headers).toEqual({
|
|
43
|
+
"X-Custom-Auth": REDACTED,
|
|
44
|
+
"Content-Type": REDACTED,
|
|
45
|
+
});
|
|
46
|
+
expect(out.url).toBe("https://mcp.example.com");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("serializeModelContext", () => {
|
|
51
|
+
it("redacts secrets in config and call settings, keeps the rest", () => {
|
|
52
|
+
const result = serializeModelContext({
|
|
53
|
+
config: { apiKey: "sk-123", baseUrl: "https://api.example.com" },
|
|
54
|
+
callSettings: {
|
|
55
|
+
maxTokens: 256,
|
|
56
|
+
headers: { Authorization: "Bearer xyz" },
|
|
57
|
+
},
|
|
58
|
+
} as never);
|
|
59
|
+
|
|
60
|
+
expect(result?.config).toEqual({
|
|
61
|
+
apiKey: REDACTED,
|
|
62
|
+
baseUrl: "https://api.example.com",
|
|
63
|
+
});
|
|
64
|
+
expect(result?.callSettings).toEqual({
|
|
65
|
+
maxTokens: 256,
|
|
66
|
+
headers: { Authorization: REDACTED },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("redacts tool providerOptions/server but never the parameters schema", () => {
|
|
71
|
+
const result = serializeModelContext({
|
|
72
|
+
tools: {
|
|
73
|
+
search: {
|
|
74
|
+
type: "frontend",
|
|
75
|
+
parameters: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: { token: { type: "string" } },
|
|
78
|
+
},
|
|
79
|
+
providerOptions: { openai: { apiKey: "sk-xyz" } },
|
|
80
|
+
},
|
|
81
|
+
mcp_tool: {
|
|
82
|
+
type: "mcp",
|
|
83
|
+
server: { type: "http", url: "https://x", headers: { token: "t" } },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as never);
|
|
87
|
+
|
|
88
|
+
const search = result?.tools?.find((t) => t.name === "search");
|
|
89
|
+
const mcpTool = result?.tools?.find((t) => t.name === "mcp_tool");
|
|
90
|
+
|
|
91
|
+
expect(search?.providerOptions).toEqual({ openai: { apiKey: REDACTED } });
|
|
92
|
+
expect(search?.parameters).toEqual({
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: { token: { type: "string" } },
|
|
95
|
+
});
|
|
96
|
+
expect(mcpTool?.server).toEqual({
|
|
97
|
+
type: "http",
|
|
98
|
+
url: "https://x",
|
|
99
|
+
headers: { token: REDACTED },
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("masks arbitrary env names on an MCP stdio server tool", () => {
|
|
104
|
+
const result = serializeModelContext({
|
|
105
|
+
tools: {
|
|
106
|
+
gh: {
|
|
107
|
+
type: "mcp",
|
|
108
|
+
server: {
|
|
109
|
+
type: "stdio",
|
|
110
|
+
command: "mcp-github",
|
|
111
|
+
env: { GITHUB_TOKEN: "ghp_secret", OPENAI_API_KEY: "sk" },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
} as never);
|
|
116
|
+
|
|
117
|
+
const gh = result?.tools?.find((t) => t.name === "gh");
|
|
118
|
+
expect(gh?.server).toEqual({
|
|
119
|
+
type: "stdio",
|
|
120
|
+
command: "mcp-github",
|
|
121
|
+
env: { GITHUB_TOKEN: REDACTED, OPENAI_API_KEY: REDACTED },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns undefined when there is no context", () => {
|
|
126
|
+
expect(serializeModelContext(undefined)).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ModelContext } from "@assistant-ui/react";
|
|
2
2
|
import type { SerializedModelContext } from "../types";
|
|
3
|
-
import { normalizeToolList } from "./toolNormalization";
|
|
3
|
+
import { normalizeToolList, type NormalizedTool } from "./toolNormalization";
|
|
4
4
|
|
|
5
5
|
export const sanitizeForMessage = (
|
|
6
6
|
value: unknown,
|
|
@@ -52,6 +52,69 @@ export const sanitizeForMessage = (
|
|
|
52
52
|
return value;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
export const REDACTED = "[redacted]";
|
|
56
|
+
|
|
57
|
+
const SENSITIVE_KEYS = new Set([
|
|
58
|
+
"apikey",
|
|
59
|
+
"xapikey",
|
|
60
|
+
"accesskey",
|
|
61
|
+
"authorization",
|
|
62
|
+
"password",
|
|
63
|
+
"passwd",
|
|
64
|
+
"secret",
|
|
65
|
+
"clientsecret",
|
|
66
|
+
"token",
|
|
67
|
+
"accesstoken",
|
|
68
|
+
"refreshtoken",
|
|
69
|
+
"cookie",
|
|
70
|
+
"setcookie",
|
|
71
|
+
"credential",
|
|
72
|
+
"credentials",
|
|
73
|
+
"privatekey",
|
|
74
|
+
"bearer",
|
|
75
|
+
"sessionid",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const normalizeKey = (key: string) => key.toLowerCase().replace(/[-_]/g, "");
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Subtrees that are credential maps with arbitrary, user-defined key names
|
|
82
|
+
* (MCP stdio `env`, HTTP `headers`). Per-key name matching cannot catch
|
|
83
|
+
* `OPENAI_API_KEY` or a custom auth header, so every leaf inside one of these
|
|
84
|
+
* is masked wholesale.
|
|
85
|
+
*/
|
|
86
|
+
const MASK_ALL_KEYS = new Set(["env", "headers"]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mask values whose key names a known credential, and mask every value inside
|
|
90
|
+
* an `env`/`headers` subtree wholesale. Operates on already sanitized plain
|
|
91
|
+
* data (primitives, arrays, plain objects). Applied only to config-bearing
|
|
92
|
+
* subtrees, never to a tool's parameters schema, so a schema property literally
|
|
93
|
+
* named `token` is not corrupted.
|
|
94
|
+
*/
|
|
95
|
+
export const redactSensitive = (value: unknown, maskAll = false): unknown => {
|
|
96
|
+
if (Array.isArray(value)) {
|
|
97
|
+
return value.map((entry) => redactSensitive(entry, maskAll));
|
|
98
|
+
}
|
|
99
|
+
if (value && typeof value === "object") {
|
|
100
|
+
const result: Record<string, unknown> = {};
|
|
101
|
+
for (const [key, entry] of Object.entries(
|
|
102
|
+
value as Record<string, unknown>,
|
|
103
|
+
)) {
|
|
104
|
+
const normalized = normalizeKey(key);
|
|
105
|
+
result[key] =
|
|
106
|
+
maskAll || SENSITIVE_KEYS.has(normalized)
|
|
107
|
+
? REDACTED
|
|
108
|
+
: redactSensitive(entry, MASK_ALL_KEYS.has(normalized));
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
return maskAll ? REDACTED : value;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const sanitizeAndRedact = (value: unknown): unknown =>
|
|
116
|
+
redactSensitive(sanitizeForMessage(value));
|
|
117
|
+
|
|
55
118
|
export const serializeModelContext = (
|
|
56
119
|
context: ModelContext | undefined,
|
|
57
120
|
): SerializedModelContext | undefined => {
|
|
@@ -69,14 +132,28 @@ export const serializeModelContext = (
|
|
|
69
132
|
|
|
70
133
|
const tools = normalizeToolList(modelContext.tools);
|
|
71
134
|
if (tools.length > 0) {
|
|
72
|
-
result.tools = tools.map((tool) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
135
|
+
result.tools = tools.map((tool): NormalizedTool => {
|
|
136
|
+
return {
|
|
137
|
+
...tool,
|
|
138
|
+
parameters: sanitizeForMessage(tool.parameters),
|
|
139
|
+
...(tool.providerOptions !== undefined
|
|
140
|
+
? { providerOptions: sanitizeAndRedact(tool.providerOptions) }
|
|
141
|
+
: {}),
|
|
142
|
+
...(tool.providerArgs !== undefined
|
|
143
|
+
? { providerArgs: sanitizeAndRedact(tool.providerArgs) }
|
|
144
|
+
: {}),
|
|
145
|
+
...(tool.server !== undefined
|
|
146
|
+
? { server: sanitizeAndRedact(tool.server) }
|
|
147
|
+
: {}),
|
|
148
|
+
...(tool.backendDefault !== undefined
|
|
149
|
+
? { backendDefault: sanitizeForMessage(tool.backendDefault) }
|
|
150
|
+
: {}),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
76
153
|
}
|
|
77
154
|
|
|
78
155
|
if (modelContext.callSettings !== undefined) {
|
|
79
|
-
const callSettings =
|
|
156
|
+
const callSettings = sanitizeAndRedact(modelContext.callSettings);
|
|
80
157
|
if (
|
|
81
158
|
callSettings &&
|
|
82
159
|
typeof callSettings === "object" &&
|
|
@@ -87,7 +164,7 @@ export const serializeModelContext = (
|
|
|
87
164
|
}
|
|
88
165
|
|
|
89
166
|
if (modelContext.config !== undefined) {
|
|
90
|
-
const config =
|
|
167
|
+
const config = sanitizeAndRedact(modelContext.config);
|
|
91
168
|
if (config && typeof config === "object" && !Array.isArray(config)) {
|
|
92
169
|
result.config = config as Record<string, unknown>;
|
|
93
170
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeToolList } from "./toolNormalization";
|
|
3
|
+
|
|
4
|
+
describe("normalizeToolList", () => {
|
|
5
|
+
it("retains the full metadata of a provider tool", () => {
|
|
6
|
+
const [tool] = normalizeToolList({
|
|
7
|
+
web_search: {
|
|
8
|
+
type: "provider",
|
|
9
|
+
providerId: "openai.web_search_preview",
|
|
10
|
+
args: { searchContextSize: "high" },
|
|
11
|
+
supportsDeferredResults: true,
|
|
12
|
+
disabled: false,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(tool).toMatchObject({
|
|
17
|
+
name: "web_search",
|
|
18
|
+
type: "provider",
|
|
19
|
+
providerId: "openai.web_search_preview",
|
|
20
|
+
providerArgs: { searchContextSize: "high" },
|
|
21
|
+
supportsDeferredResults: true,
|
|
22
|
+
disabled: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("retains the MCP server config and display mode", () => {
|
|
27
|
+
const [tool] = normalizeToolList({
|
|
28
|
+
list_repos: {
|
|
29
|
+
type: "mcp",
|
|
30
|
+
display: "standalone",
|
|
31
|
+
server: { type: "http", url: "https://mcp.example.com" },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(tool?.type).toBe("mcp");
|
|
36
|
+
expect(tool?.display).toBe("standalone");
|
|
37
|
+
expect(tool?.server).toEqual({
|
|
38
|
+
type: "http",
|
|
39
|
+
url: "https://mcp.example.com",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("retains providerOptions and backend defaults", () => {
|
|
44
|
+
const [tool] = normalizeToolList({
|
|
45
|
+
submit: {
|
|
46
|
+
type: "frontend",
|
|
47
|
+
parameters: { type: "object" },
|
|
48
|
+
providerOptions: { openai: { strict: true } },
|
|
49
|
+
unstable_backendDefault: { parameters: true },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(tool?.providerOptions).toEqual({ openai: { strict: true } });
|
|
54
|
+
expect(tool?.backendDefault).toEqual({ parameters: true });
|
|
55
|
+
expect(tool?.parameters).toEqual({ type: "object" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("handles the array form", () => {
|
|
59
|
+
const tools = normalizeToolList([
|
|
60
|
+
{ name: "a", type: "frontend" },
|
|
61
|
+
{ name: "b", type: "backend", disabled: true },
|
|
62
|
+
]);
|
|
63
|
+
expect(tools.map((t) => t.name)).toEqual(["a", "b"]);
|
|
64
|
+
expect(tools[1]?.disabled).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns an empty list for non-objects", () => {
|
|
68
|
+
expect(normalizeToolList(undefined)).toEqual([]);
|
|
69
|
+
expect(normalizeToolList(null)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -5,6 +5,13 @@ export type NormalizedTool = {
|
|
|
5
5
|
type?: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
display?: string;
|
|
9
|
+
providerId?: string;
|
|
10
|
+
supportsDeferredResults?: boolean;
|
|
11
|
+
backendDefault?: unknown;
|
|
12
|
+
providerOptions?: unknown;
|
|
13
|
+
providerArgs?: unknown;
|
|
14
|
+
server?: unknown;
|
|
8
15
|
parameters?: unknown;
|
|
9
16
|
};
|
|
10
17
|
|
|
@@ -41,6 +48,34 @@ const mapToNormalizedTool = (
|
|
|
41
48
|
tool.disabled = raw.disabled as boolean;
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
if (typeof raw.display === "string") {
|
|
52
|
+
tool.display = raw.display as string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof raw.providerId === "string") {
|
|
56
|
+
tool.providerId = raw.providerId as string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof raw.supportsDeferredResults === "boolean") {
|
|
60
|
+
tool.supportsDeferredResults = raw.supportsDeferredResults as boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (raw.unstable_backendDefault !== undefined) {
|
|
64
|
+
tool.backendDefault = raw.unstable_backendDefault;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (raw.providerOptions !== undefined) {
|
|
68
|
+
tool.providerOptions = raw.providerOptions;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (raw.args !== undefined) {
|
|
72
|
+
tool.providerArgs = raw.args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (raw.server !== undefined) {
|
|
76
|
+
tool.server = raw.server;
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
if (Object.hasOwn(raw, "parameters")) {
|
|
45
80
|
tool.parameters = toJsonSchema(raw.parameters);
|
|
46
81
|
}
|