/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/ModelContextService.h" #include "js/JSON.h" #include "jsapi.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/ErrorResult.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/AbortSignal.h" #include "mozilla/dom/AbortSignalBinding.h" #include "mozilla/dom/ModelContext.h" #include "mozilla/dom/ModelContextBinding.h" #include "mozilla/dom/ModelContextTypes.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/ipc/StructuredCloneData.h" #include "xpcpublic.h" namespace mozilla::dom { StaticRefPtr ModelContextService::sSingleton; NS_IMPL_ISUPPORTS(ModelContextService, nsIModelContextService) /* static */ already_AddRefed ModelContextService::GetSingleton() { if (!sSingleton) { sSingleton = new ModelContextService(); ClearOnShutdown(&sSingleton); } return do_AddRef(sSingleton); } NS_IMETHODIMP ModelContextService::GetToolsForWindow(uint64_t aInnerWindowId, JSContext* aCx, Promise** aResult) { MOZ_ASSERT(XRE_IsParentProcess()); if (!XRE_IsParentProcess()) { return NS_ERROR_NOT_AVAILABLE; } if (!StaticPrefs::dom_modelcontext_enabled()) { return NS_ERROR_NOT_AVAILABLE; } nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); if (!global) { return NS_ERROR_UNEXPECTED; } ErrorResult erv; RefPtr promise = Promise::Create(global, erv); if (erv.Failed()) { return erv.StealNSResult(); } RefPtr wgp = WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); if (!wgp) { promise->MaybeRejectWithNotFoundError("Window not found"_ns); promise.forget(aResult); return NS_OK; } RefPtr ipcPromise = wgp->SendGetModelContextTools(); ipcPromise->Then( GetMainThreadSerialEventTarget(), __func__, [promise](const PWindowGlobalParent::GetModelContextToolsPromise:: ResolveValueType& aResult) { const auto& [rv, tools] = aResult; if (NS_FAILED(rv)) { promise->MaybeReject(rv); return; } AutoJSAPI jsapi; if (!jsapi.Init(promise->GetGlobalObject())) { promise->MaybeRejectWithUnknownError("Failed to init JS context"_ns); return; } JSContext* cx = jsapi.cx(); JS::Rooted array(cx, JS::NewArrayObject(cx, tools.Length())); if (!array) { promise->MaybeRejectWithUnknownError( "Failed to create result array"_ns); return; } for (uint32_t i = 0; i < tools.Length(); ++i) { RootedDictionary def(cx); def.mName = NS_ConvertUTF8toUTF16(tools[i].name()); def.mDescription = NS_ConvertUTF8toUTF16(tools[i].description()); if (tools[i].inputSchema().isSome()) { NS_ConvertUTF8toUTF16 schema(tools[i].inputSchema().ref()); JS::Rooted schemaVal(cx); if (!JS_ParseJSON(cx, schema.get(), schema.Length(), &schemaVal)) { promise->MaybeRejectWithUnknownError( "Failed to parse inputSchema"_ns); return; } if (!schemaVal.isObject()) { promise->MaybeRejectWithInvalidStateError( "Tool stored with non-object inputSchema"_ns); return; } def.mInputSchema.Construct(&schemaVal.toObject()); } if (tools[i].annotations().isSome()) { def.mAnnotations.Construct(); if (tools[i].annotations()->readOnlyHint().isSome()) { def.mAnnotations.Value().mReadOnlyHint.Construct( tools[i].annotations()->readOnlyHint().value()); } } JS::Rooted val(cx); if (!ToJSValue(cx, def, &val)) { promise->MaybeRejectWithUnknownError( "Failed to convert tool definition"_ns); return; } if (!JS_DefineElement(cx, array, i, val, JSPROP_ENUMERATE)) { promise->MaybeRejectWithUnknownError( "Failed to add tool to array"_ns); return; } } JS::Rooted arrayVal(cx, JS::ObjectValue(*array)); promise->MaybeResolve(arrayVal); }, [promise](mozilla::ipc::ResponseRejectReason aReason) { promise->MaybeRejectWithAbortError("IPC failed"_ns); }); promise.forget(aResult); return NS_OK; } NS_IMETHODIMP ModelContextService::InvokeTool(uint64_t aInnerWindowId, const nsACString& aToolName, JS::Handle aInput, JSContext* aCx, Promise** aResult) { MOZ_ASSERT(XRE_IsParentProcess()); if (!XRE_IsParentProcess()) { return NS_ERROR_NOT_AVAILABLE; } if (!StaticPrefs::dom_modelcontext_enabled()) { return NS_ERROR_NOT_AVAILABLE; } nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); if (!global) { return NS_ERROR_UNEXPECTED; } ErrorResult erv; RefPtr promise = Promise::Create(global, erv); if (erv.Failed()) { return erv.StealNSResult(); } RefPtr wgp = WindowGlobalParent::GetByInnerWindowId(aInnerWindowId); if (!wgp) { promise->MaybeRejectWithNotFoundError("Window not found"_ns); promise.forget(aResult); return NS_OK; } RefPtr inputClone = MakeRefPtr(); IgnoredErrorResult writeRv; inputClone->Write(aCx, aInput, writeRv); if (writeRv.Failed()) { promise->MaybeRejectWithDataCloneError("Failed to serialize input"_ns); promise.forget(aResult); return NS_OK; } RefPtr ipcPromise = wgp->SendInvokeModelContextTool(aToolName, WrapNotNull(inputClone)); ipcPromise->Then( GetMainThreadSerialEventTarget(), __func__, [promise](const PWindowGlobalParent::InvokeModelContextToolPromise:: ResolveValueType& aResult) { const auto& [rv, output] = aResult; if (rv.Failed()) { if (output) { AutoJSAPI jsapi; if (jsapi.Init(promise->GetGlobalObject())) { JSContext* cx = jsapi.cx(); JS::Rooted rejectionValue(cx); IgnoredErrorResult deserializeRv; output->Read(cx, &rejectionValue, deserializeRv); if (!deserializeRv.Failed()) { promise->MaybeReject(rejectionValue); return; } } } CopyableErrorResult rvCopy(rv); promise->MaybeReject(std::move(rvCopy)); return; } AutoJSAPI jsapi; if (!jsapi.Init(promise->GetGlobalObject())) { promise->MaybeRejectWithUnknownError("Failed to init JS context"_ns); return; } JSContext* cx = jsapi.cx(); JS::Rooted resultValue(cx); IgnoredErrorResult deserializeRv; output->Read(cx, &resultValue, deserializeRv); if (deserializeRv.Failed()) { promise->MaybeRejectWithDataCloneError( "Failed to deserialize result"_ns); return; } promise->MaybeResolve(resultValue); }, [promise](mozilla::ipc::ResponseRejectReason aReason) { promise->MaybeRejectWithAbortError("tool invocation failed"_ns); }); promise.forget(aResult); return NS_OK; } } // namespace mozilla::dom