@ateam-ai-mobile/plugin-runtime 0.1.7 → 0.1.9
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/{chunk-PKDQLNBL.mjs → chunk-SICFDA5K.mjs} +388 -70
- package/dist/chunk-SICFDA5K.mjs.map +1 -0
- package/dist/index.d.mts +462 -46
- package/dist/index.d.ts +462 -46
- package/dist/index.js +429 -230
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -157
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-5LUVXUY2.mjs → loader-MSZ4PVTO.mjs} +2 -2
- package/package.json +2 -2
- package/src/api/plugins.ts +15 -9
- package/src/bundles/bundleManager.ts +187 -64
- package/src/bundles/bundleModuleNames.ts +52 -0
- package/src/bundles/bundleUrl.ts +41 -0
- package/src/bundles/storage.ts +71 -0
- package/src/deviceStateHost.ts +25 -0
- package/src/hooks/usePlugin.ts +15 -4
- package/src/host.ts +2 -0
- package/src/index.ts +16 -37
- package/src/pluginRuntime.ts +22 -11
- package/src/sdk/CameraPreview.tsx +7 -0
- package/src/sdk/device-state.ts +120 -17
- package/src/sdk/index.ts +41 -0
- package/src/sdk/registration.ts +2 -1
- package/src/sdk/types.ts +162 -5
- package/src/sdk/voice.ts +82 -0
- package/src/tools/loader.ts +28 -8
- package/src/tools/registry.ts +14 -7
- package/src/voiceHost.ts +2 -0
- package/dist/chunk-PKDQLNBL.mjs.map +0 -1
- /package/dist/{loader-5LUVXUY2.mjs.map → loader-MSZ4PVTO.mjs.map} +0 -0
|
@@ -7,6 +7,23 @@ var __export = (target, all) => {
|
|
|
7
7
|
// src/api/plugins.ts
|
|
8
8
|
import { apiFetch } from "@ateam-ai-mobile/core";
|
|
9
9
|
import { getCoreApiUrl } from "@ateam-ai-mobile/core";
|
|
10
|
+
|
|
11
|
+
// src/bundles/bundleUrl.ts
|
|
12
|
+
function pluginBundleUrl(pluginId) {
|
|
13
|
+
return `/api/ui-plugins/${encodeURIComponent(pluginId)}/bundle.js`;
|
|
14
|
+
}
|
|
15
|
+
function isPluginBundleUrl(url, pluginId) {
|
|
16
|
+
if (typeof url !== "string") return false;
|
|
17
|
+
const canonical = pluginBundleUrl(pluginId);
|
|
18
|
+
if (url === canonical) return true;
|
|
19
|
+
try {
|
|
20
|
+
return decodeURIComponent(url) === decodeURIComponent(canonical);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/api/plugins.ts
|
|
10
27
|
function supportsNative(manifest) {
|
|
11
28
|
const mode = manifest.render?.mode;
|
|
12
29
|
return mode === "react-native" || mode === "adaptive";
|
|
@@ -17,12 +34,9 @@ function getNativeComponentId(manifest) {
|
|
|
17
34
|
return null;
|
|
18
35
|
}
|
|
19
36
|
function getNativeBundleUrl(manifest) {
|
|
20
|
-
const direct = manifest.bundleUrl;
|
|
21
|
-
if (direct) return direct;
|
|
22
37
|
const render = manifest.render;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return null;
|
|
38
|
+
const declared = manifest.bundleUrl || (render?.mode === "react-native" ? render.bundleUrl : null) || (render?.mode === "adaptive" ? render.reactNative?.bundleUrl : null);
|
|
39
|
+
return declared && manifest.id ? pluginBundleUrl(manifest.id) : null;
|
|
26
40
|
}
|
|
27
41
|
function getNativeBundleHash(manifest) {
|
|
28
42
|
const direct = manifest.bundleHash;
|
|
@@ -155,6 +169,8 @@ __export(sdk_exports, {
|
|
|
155
169
|
TextInput: () => TextInput,
|
|
156
170
|
TouchableOpacity: () => TouchableOpacity,
|
|
157
171
|
View: () => View,
|
|
172
|
+
explainCapability: () => explainCapability,
|
|
173
|
+
isActionable: () => isActionable,
|
|
158
174
|
register: () => register,
|
|
159
175
|
registerService: () => registerService,
|
|
160
176
|
registerTools: () => registerTools,
|
|
@@ -197,6 +213,60 @@ function registerService(id, config) {
|
|
|
197
213
|
// src/sdk/voice.ts
|
|
198
214
|
var PLATFORM_VOICE_CALLER = /* @__PURE__ */ Symbol("adas.voice.platform");
|
|
199
215
|
|
|
216
|
+
// src/sdk/device-state.ts
|
|
217
|
+
var device_state_exports = {};
|
|
218
|
+
__export(device_state_exports, {
|
|
219
|
+
DeviceBridgeError: () => DeviceBridgeError,
|
|
220
|
+
explainCapability: () => explainCapability,
|
|
221
|
+
isActionable: () => isActionable,
|
|
222
|
+
neverObserved: () => neverObserved
|
|
223
|
+
});
|
|
224
|
+
var DeviceBridgeError = class extends Error {
|
|
225
|
+
constructor(code, message, opts) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.name = "DeviceBridgeError";
|
|
228
|
+
this.code = code;
|
|
229
|
+
this.domain = opts?.domain;
|
|
230
|
+
this.recoverable = opts?.recoverable;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var CONFIDENCE_ORDER = {
|
|
234
|
+
unknown: 0,
|
|
235
|
+
low: 1,
|
|
236
|
+
medium: 2,
|
|
237
|
+
high: 3
|
|
238
|
+
};
|
|
239
|
+
function isActionable(quality, budget) {
|
|
240
|
+
const { freshness, confidence } = quality;
|
|
241
|
+
if (freshness.state === "unknown" || freshness.state === "stale") return false;
|
|
242
|
+
if (freshness.ageMs === null || freshness.ageMs > budget.maxAgeMs) return false;
|
|
243
|
+
if (budget.minConfidence) {
|
|
244
|
+
const have = CONFIDENCE_ORDER[confidence?.level ?? "unknown"];
|
|
245
|
+
if (have < CONFIDENCE_ORDER[budget.minConfidence]) return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
function neverObserved() {
|
|
250
|
+
return { state: "unknown", observedAt: null, ageMs: null };
|
|
251
|
+
}
|
|
252
|
+
function explainCapability(status) {
|
|
253
|
+
if (status.support === "unsupported") return "this device does not have it";
|
|
254
|
+
if (status.permission === "denied") return "permission was denied";
|
|
255
|
+
if (status.permission === "restricted") return "permission is restricted";
|
|
256
|
+
if (status.permission === "notDetermined") return "permission has not been asked for yet";
|
|
257
|
+
if (status.permission === "limited") return "only limited access was granted";
|
|
258
|
+
if (status.restriction === "policyRestricted") return "this plugin did not declare it";
|
|
259
|
+
if (status.restriction === "backgroundRestricted") return "the app is in the background";
|
|
260
|
+
if (status.restriction === "lowPowerRestricted") return "the device is in low-power mode";
|
|
261
|
+
if (status.availability === "busy") return "it is in use by something else";
|
|
262
|
+
if (status.availability === "temporarilyUnavailable") return "it is temporarily unavailable";
|
|
263
|
+
if (status.availability === "unavailable") return "it is unavailable";
|
|
264
|
+
if (status.support === "unknown" || status.availability === "unknown") {
|
|
265
|
+
return "its state could not be determined";
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
200
270
|
// src/sdk/primitives.ts
|
|
201
271
|
import {
|
|
202
272
|
View,
|
|
@@ -511,7 +581,8 @@ var noopNative = {
|
|
|
511
581
|
scanner: unavailable("scanner"),
|
|
512
582
|
nfc: unavailable("nfc"),
|
|
513
583
|
bluetooth: unavailable("bluetooth"),
|
|
514
|
-
vision: unavailable("vision")
|
|
584
|
+
vision: unavailable("vision"),
|
|
585
|
+
maps: unavailable("maps")
|
|
515
586
|
};
|
|
516
587
|
var _pluginNative = noopNative;
|
|
517
588
|
function setPluginNativeProvider(native) {
|
|
@@ -553,7 +624,8 @@ function gatePluginNative(native, declared, pluginId) {
|
|
|
553
624
|
// every method refuses by name. Both are better than the `undefined` this
|
|
554
625
|
// used to be, which crashed with "cannot read properties of undefined".
|
|
555
626
|
bluetooth: caps.bluetooth ? native.bluetooth : blocked("bluetooth"),
|
|
556
|
-
vision: caps.vision ? native.vision : blocked("vision")
|
|
627
|
+
vision: caps.vision ? native.vision : blocked("vision"),
|
|
628
|
+
maps: caps.maps ? native.maps : blocked("maps")
|
|
557
629
|
// What this plugin DECLARED, carried on the object it already receives.
|
|
558
630
|
//
|
|
559
631
|
// It is how a COMPONENT can be capability-gated: <CameraPreview> is handed
|
|
@@ -1561,6 +1633,14 @@ function SurfaceDispatcher() {
|
|
|
1561
1633
|
] });
|
|
1562
1634
|
}
|
|
1563
1635
|
|
|
1636
|
+
// src/bundles/bundleModuleNames.ts
|
|
1637
|
+
var PLUGIN_SDK_MODULE = "@adas/plugin-sdk";
|
|
1638
|
+
function sdkModuleRefusal(name) {
|
|
1639
|
+
if (name === PLUGIN_SDK_MODULE) return null;
|
|
1640
|
+
if (!name.startsWith("@ateam-ai-mobile/") && !name.startsWith("@adas/")) return null;
|
|
1641
|
+
return `a plugin bundle required '${name}', which does not exist inside a bundle. Import from '${PLUGIN_SDK_MODULE}' instead \u2014 it is the only SDK module a bundle can require (values and types alike), and the one to keep external when building. '@ateam-ai-mobile/*' are the host app's packages, not the plugin's.`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1564
1644
|
// src/utils/sha256.ts
|
|
1565
1645
|
var K = new Uint32Array([
|
|
1566
1646
|
1116352408,
|
|
@@ -1715,6 +1795,22 @@ function selectOwnedCacheEntries(entries, tenant) {
|
|
|
1715
1795
|
if (!tenant) return [];
|
|
1716
1796
|
return (entries || []).filter((e) => !!e && e.tenant === tenant);
|
|
1717
1797
|
}
|
|
1798
|
+
async function loadCacheIndex(storage = getBundleStorage()) {
|
|
1799
|
+
const index = await storage.loadIndex() || {};
|
|
1800
|
+
for (const [key, entry] of Object.entries(index)) {
|
|
1801
|
+
const pluginId = entry?.trusted?.pluginId;
|
|
1802
|
+
if (!pluginId) continue;
|
|
1803
|
+
const canonical = pluginBundleUrl(pluginId);
|
|
1804
|
+
if (key === canonical) continue;
|
|
1805
|
+
if (!isPluginBundleUrl(key, pluginId) || !isPluginBundleUrl(entry.bundleUrl, pluginId)) continue;
|
|
1806
|
+
delete index[key];
|
|
1807
|
+
const held = index[canonical];
|
|
1808
|
+
if (!held || (entry.timestamp ?? 0) > (held.timestamp ?? 0)) {
|
|
1809
|
+
index[canonical] = { ...entry, bundleUrl: canonical };
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return index;
|
|
1813
|
+
}
|
|
1718
1814
|
var MemoryBundleStorage = class {
|
|
1719
1815
|
constructor() {
|
|
1720
1816
|
this.index = {};
|
|
@@ -2104,6 +2200,19 @@ function gateDeviceState(declared) {
|
|
|
2104
2200
|
}
|
|
2105
2201
|
return out;
|
|
2106
2202
|
}
|
|
2203
|
+
function withoutSubscribe(state) {
|
|
2204
|
+
if (!state) return state;
|
|
2205
|
+
const out = {};
|
|
2206
|
+
for (const domain of ALL_DOMAINS) {
|
|
2207
|
+
const d = state[domain];
|
|
2208
|
+
out[domain] = d && Object.assign(Object.create(d), {
|
|
2209
|
+
subscribe() {
|
|
2210
|
+
throw denial(domain, "a device tool call has no lifetime to own a subscription. Subscribe from the plugin UI, or read getSnapshot() within the call.");
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
return out;
|
|
2215
|
+
}
|
|
2107
2216
|
function implementedStateDomains() {
|
|
2108
2217
|
return ALL_DOMAINS.filter((d) => !!_provider[d]);
|
|
2109
2218
|
}
|
|
@@ -2149,8 +2258,9 @@ function unregisterDeviceTools(pluginId) {
|
|
|
2149
2258
|
if (entry.pluginId === pluginId) registry.delete(name);
|
|
2150
2259
|
}
|
|
2151
2260
|
}
|
|
2152
|
-
function hasDeviceTool(name) {
|
|
2153
|
-
|
|
2261
|
+
function hasDeviceTool(name, pluginId) {
|
|
2262
|
+
const entry = registry.get(name);
|
|
2263
|
+
return !!entry && (pluginId === void 0 || entry.pluginId === pluginId);
|
|
2154
2264
|
}
|
|
2155
2265
|
function listDeviceTools() {
|
|
2156
2266
|
return Array.from(registry.entries()).map(([name, { pluginId, tool }]) => ({
|
|
@@ -2169,8 +2279,11 @@ async function callDeviceTool(name, args = {}, opts = {}) {
|
|
|
2169
2279
|
let timer;
|
|
2170
2280
|
try {
|
|
2171
2281
|
const build = contextProvider || defaultContext;
|
|
2282
|
+
const built = build(entry.pluginId, entry.capabilities, entry.stateDomains);
|
|
2172
2283
|
const ctx = {
|
|
2173
|
-
...
|
|
2284
|
+
...built,
|
|
2285
|
+
// Whoever built the context, a call cannot own a subscription.
|
|
2286
|
+
deviceState: withoutSubscribe(built.deviceState),
|
|
2174
2287
|
signal: opts.signal
|
|
2175
2288
|
};
|
|
2176
2289
|
const result = await Promise.race([
|
|
@@ -2196,6 +2309,9 @@ function clearDeviceTools() {
|
|
|
2196
2309
|
}
|
|
2197
2310
|
|
|
2198
2311
|
// src/bundles/bundleManager.ts
|
|
2312
|
+
function bundleKey(pluginId, bundleUrl) {
|
|
2313
|
+
return isPluginBundleUrl(bundleUrl, pluginId) ? pluginBundleUrl(pluginId) : bundleUrl;
|
|
2314
|
+
}
|
|
2199
2315
|
var ReactShim = {
|
|
2200
2316
|
__esModule: true,
|
|
2201
2317
|
default: React2,
|
|
@@ -2241,13 +2357,17 @@ var PluginSDKShim = Object.freeze({
|
|
|
2241
2357
|
var requireShim = (name) => {
|
|
2242
2358
|
if (name === "react") return ReactShim;
|
|
2243
2359
|
if (name === "react-native") return ReactNative;
|
|
2244
|
-
if (name ===
|
|
2360
|
+
if (name === PLUGIN_SDK_MODULE) return PluginSDKShim;
|
|
2361
|
+
const refusal = sdkModuleRefusal(name);
|
|
2362
|
+
if (refusal) throw new Error(`[BundleManager] ${refusal}`);
|
|
2245
2363
|
const hostModule = resolveBundleModule(name);
|
|
2246
2364
|
if (hostModule !== void 0) return hostModule;
|
|
2247
2365
|
console.warn(`[BundleManager] Unknown module: ${name} \u2014 register it via registerBundleModules()`);
|
|
2248
2366
|
return {};
|
|
2249
2367
|
};
|
|
2250
2368
|
var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
|
|
2369
|
+
var hasComponent = (val) => !!val && typeof val === "object" && typeof val.Component === "function";
|
|
2370
|
+
var hasTools = (val) => !!val && typeof val === "object" && Array.isArray(val.tools) && val.tools.length > 0;
|
|
2251
2371
|
var BundleManager = class {
|
|
2252
2372
|
constructor() {
|
|
2253
2373
|
/** In-memory module cache: bundleUrl → evaluated module.exports. Holds ONE
|
|
@@ -2278,6 +2398,9 @@ var BundleManager = class {
|
|
|
2278
2398
|
* of the previous tenant's tools.
|
|
2279
2399
|
*/
|
|
2280
2400
|
this.registeredToolPluginIds = /* @__PURE__ */ new Set();
|
|
2401
|
+
/** bundleUrl → the trusted declaration last recorded on its disk-cache entry
|
|
2402
|
+
* this session, so a memory hit with unchanged grants skips the index I/O. */
|
|
2403
|
+
this.recordedDeclarations = /* @__PURE__ */ new Map();
|
|
2281
2404
|
/**
|
|
2282
2405
|
* Enforce the in-memory tenant boundary. Call at the top of every public load
|
|
2283
2406
|
* path, BEFORE any cache read.
|
|
@@ -2323,6 +2446,7 @@ var BundleManager = class {
|
|
|
2323
2446
|
}
|
|
2324
2447
|
}
|
|
2325
2448
|
this.registeredToolPluginIds.clear();
|
|
2449
|
+
this.recordedDeclarations.clear();
|
|
2326
2450
|
this.modules.clear();
|
|
2327
2451
|
this.provenance.clear();
|
|
2328
2452
|
this.inflight.clear();
|
|
@@ -2334,13 +2458,15 @@ var BundleManager = class {
|
|
|
2334
2458
|
isStale(epoch, tenant) {
|
|
2335
2459
|
return this.epoch !== epoch || getTenant2() !== tenant;
|
|
2336
2460
|
}
|
|
2337
|
-
/**
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
|
|
2461
|
+
/** Record a memory serve: the running bytes are the already-evaluated module,
|
|
2462
|
+
* and `expected` is refreshed from the manifest hash the caller JUST passed —
|
|
2463
|
+
* so after a redeploy the record reads hash !== expected (STALE) instead of
|
|
2464
|
+
* the old in-sync pair. The ONE place both memory paths (load()'s early return
|
|
2465
|
+
* and loadModule's) record reuse. No-op if nothing was recorded yet. */
|
|
2466
|
+
markMemoryHit(bundleUrl, expectedHash) {
|
|
2341
2467
|
const prev = this.provenance.get(bundleUrl);
|
|
2342
|
-
if (prev
|
|
2343
|
-
this.provenance.set(bundleUrl, { ...prev, source: "memory" });
|
|
2468
|
+
if (prev) {
|
|
2469
|
+
this.provenance.set(bundleUrl, { ...prev, source: "memory", expected: expectedHash?.toLowerCase() ?? prev.expected });
|
|
2344
2470
|
}
|
|
2345
2471
|
}
|
|
2346
2472
|
// ── Public API ──────────────────────────────────────────────
|
|
@@ -2352,13 +2478,22 @@ var BundleManager = class {
|
|
|
2352
2478
|
* provided for any bundle the user has not previously verified — without
|
|
2353
2479
|
* it, a tampered bundle is `eval`'d as JS with full RN runtime + every
|
|
2354
2480
|
* native capability the manifest declares.
|
|
2481
|
+
*
|
|
2482
|
+
* `grants` is what the MANIFEST grants this plugin (resolvePluginGrants) — the
|
|
2483
|
+
* only authority its device tools are registered under. Omitted (e.g. a
|
|
2484
|
+
* prewarm from the slim plugin list) → the bundle is loaded but its tools are
|
|
2485
|
+
* NOT registered: without the manifest there is nothing trusted to gate them
|
|
2486
|
+
* on, and registering with nothing would downgrade a registration the
|
|
2487
|
+
* manifest-driven tool loader already made.
|
|
2355
2488
|
*/
|
|
2356
|
-
async load(pluginId, bundleUrl, version, expectedHash) {
|
|
2489
|
+
async load(pluginId, bundleUrl, version, expectedHash, grants) {
|
|
2490
|
+
bundleUrl = bundleKey(pluginId, bundleUrl);
|
|
2357
2491
|
const tenant = this.requireTenant();
|
|
2358
2492
|
const epoch = this.epoch;
|
|
2359
2493
|
const cached = this.getComponentFromMemory(pluginId, bundleUrl);
|
|
2360
2494
|
if (cached) {
|
|
2361
|
-
this.markMemoryHit(bundleUrl);
|
|
2495
|
+
this.markMemoryHit(bundleUrl, expectedHash);
|
|
2496
|
+
if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
|
|
2362
2497
|
return cached;
|
|
2363
2498
|
}
|
|
2364
2499
|
let modulePromise = this.inflight.get(bundleUrl);
|
|
@@ -2375,6 +2510,7 @@ var BundleManager = class {
|
|
|
2375
2510
|
if (this.isStale(epoch, tenant)) {
|
|
2376
2511
|
throw new Error("[BundleManager] tenant changed while loading \u2014 discarding (fail closed)");
|
|
2377
2512
|
}
|
|
2513
|
+
if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
|
|
2378
2514
|
const component = this.getComponentFromMemory(pluginId, bundleUrl);
|
|
2379
2515
|
if (!component) {
|
|
2380
2516
|
throw new Error(
|
|
@@ -2389,8 +2525,15 @@ var BundleManager = class {
|
|
|
2389
2525
|
* Same download, same integrity check, same evaluation as `load()`; the only
|
|
2390
2526
|
* difference is that a bundle exporting tools and no `Component` is a success
|
|
2391
2527
|
* here rather than "no component found". Returns the tool names registered.
|
|
2528
|
+
*
|
|
2529
|
+
* `grants` is REQUIRED: the tools are registered under `pluginId` with exactly
|
|
2530
|
+
* what the manifest grants (see load()), never what the bundle claims.
|
|
2392
2531
|
*/
|
|
2393
|
-
async loadTools(pluginId, bundleUrl, version, expectedHash) {
|
|
2532
|
+
async loadTools(pluginId, bundleUrl, version, expectedHash, grants) {
|
|
2533
|
+
if (!grants) {
|
|
2534
|
+
throw new Error(`[BundleManager] loadTools(${pluginId}) needs the manifest's grants (resolvePluginGrants) \u2014 refusing to register tools on the bundle's own claim`);
|
|
2535
|
+
}
|
|
2536
|
+
bundleUrl = bundleKey(pluginId, bundleUrl);
|
|
2394
2537
|
const tenant = this.requireTenant();
|
|
2395
2538
|
const epoch = this.epoch;
|
|
2396
2539
|
let modulePromise = this.inflight.get(bundleUrl);
|
|
@@ -2407,6 +2550,7 @@ var BundleManager = class {
|
|
|
2407
2550
|
if (this.isStale(epoch, tenant)) {
|
|
2408
2551
|
throw new Error("[BundleManager] tenant changed while loading tools \u2014 discarding (fail closed)");
|
|
2409
2552
|
}
|
|
2553
|
+
await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
|
|
2410
2554
|
const names = listDeviceTools().filter((t) => t.pluginId === pluginId).map((t) => t.name);
|
|
2411
2555
|
if (names.length === 0) {
|
|
2412
2556
|
console.warn(
|
|
@@ -2432,7 +2576,7 @@ var BundleManager = class {
|
|
|
2432
2576
|
const it = queue.shift();
|
|
2433
2577
|
if (!it) return;
|
|
2434
2578
|
try {
|
|
2435
|
-
await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash);
|
|
2579
|
+
await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash, it.grants);
|
|
2436
2580
|
okCount++;
|
|
2437
2581
|
} catch (err) {
|
|
2438
2582
|
failCount++;
|
|
@@ -2460,11 +2604,12 @@ var BundleManager = class {
|
|
|
2460
2604
|
*/
|
|
2461
2605
|
async clearPlugin(pluginId) {
|
|
2462
2606
|
const storage = getBundleStorage();
|
|
2463
|
-
const index = await storage
|
|
2464
|
-
const
|
|
2607
|
+
const index = await loadCacheIndex(storage);
|
|
2608
|
+
const bundleUrl = pluginBundleUrl(pluginId);
|
|
2609
|
+
const entry = index[bundleUrl];
|
|
2465
2610
|
if (entry) {
|
|
2466
|
-
this.modules.delete(
|
|
2467
|
-
delete index[
|
|
2611
|
+
this.modules.delete(bundleUrl);
|
|
2612
|
+
delete index[bundleUrl];
|
|
2468
2613
|
await storage.saveIndex(index);
|
|
2469
2614
|
try {
|
|
2470
2615
|
await storage.deleteBundle(entry.localPath);
|
|
@@ -2489,12 +2634,11 @@ var BundleManager = class {
|
|
|
2489
2634
|
throw new Error("[BundleManager] tenant changed before load started \u2014 discarding (fail closed)");
|
|
2490
2635
|
}
|
|
2491
2636
|
if (this.modules.has(bundleUrl)) {
|
|
2492
|
-
|
|
2493
|
-
if (prev) this.provenance.set(bundleUrl, { ...prev, source: "memory", expected: expectedHash?.toLowerCase() ?? prev.expected });
|
|
2637
|
+
this.markMemoryHit(bundleUrl, expectedHash);
|
|
2494
2638
|
return;
|
|
2495
2639
|
}
|
|
2496
2640
|
const storage = getBundleStorage();
|
|
2497
|
-
const index = await storage
|
|
2641
|
+
const index = await loadCacheIndex(storage);
|
|
2498
2642
|
const expectedLc = expectedHash ? expectedHash.toLowerCase() : null;
|
|
2499
2643
|
let content = null;
|
|
2500
2644
|
let verifiedHash = null;
|
|
@@ -2557,30 +2701,90 @@ var BundleManager = class {
|
|
|
2557
2701
|
console.log(
|
|
2558
2702
|
`[BundleManager] RUNNING ${bundleUrl} v${version} sha=${(verifiedHash || "none").slice(0, 12)} (${servedFrom}${expectedLc && verifiedHash && expectedLc !== verifiedHash ? `, EXPECTED ${expectedLc.slice(0, 12)} \u2014 STALE` : ""})`
|
|
2559
2703
|
);
|
|
2560
|
-
this.registerToolsFromModule(mod, bundleUrl);
|
|
2561
2704
|
}
|
|
2562
2705
|
/**
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2565
|
-
*
|
|
2706
|
+
* Register THIS plugin's device tools under the TRUSTED declaration, then
|
|
2707
|
+
* record that declaration on the disk-cache entry. Runs on every trusted load
|
|
2708
|
+
* — memory hit included — so the grants in force are always the latest the
|
|
2709
|
+
* manifest gave (a revoked grant stops applying on the next load, not the next
|
|
2710
|
+
* app start). Callers run it only after their stale check.
|
|
2711
|
+
*/
|
|
2712
|
+
async commitTools(pluginId, bundleUrl, grants, tenant, epoch) {
|
|
2713
|
+
const declaration = {
|
|
2714
|
+
pluginId,
|
|
2715
|
+
capabilities: grants.capabilities,
|
|
2716
|
+
stateDomains: grants.stateDomains
|
|
2717
|
+
};
|
|
2718
|
+
this.registerToolsFromModule(this.modules.get(bundleUrl), declaration);
|
|
2719
|
+
await this.recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch);
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Put the plugin's own `tools` section in the device tool registry — under the
|
|
2723
|
+
* pluginId and grants the MANIFEST resolved, never the bundle's own `id`,
|
|
2724
|
+
* `capabilities` or `stateDomains`. A bundle is code the manifest authorised
|
|
2725
|
+
* by hash; what it may touch is the manifest's to say, same as its rendered
|
|
2726
|
+
* surface (resolvePluginRuntimeProps). Its own `id` would also let it register
|
|
2727
|
+
* as another plugin — including the host's platform tools, which a same-id
|
|
2728
|
+
* re-registration replaces.
|
|
2729
|
+
*
|
|
2730
|
+
* Only this plugin's export (the same lookup the component uses) is
|
|
2731
|
+
* registered. Another export's tools in the same bundle belong to another
|
|
2732
|
+
* plugin and register when THAT plugin's manifest loads it.
|
|
2566
2733
|
*/
|
|
2567
|
-
registerToolsFromModule(mod,
|
|
2734
|
+
registerToolsFromModule(mod, { pluginId, capabilities, stateDomains }) {
|
|
2568
2735
|
if (!mod) return;
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2736
|
+
const own = this.findPluginExport(mod, pluginId, hasTools);
|
|
2737
|
+
const others = Object.keys(mod).filter((k) => hasTools(mod[k]) && mod[k] !== own);
|
|
2738
|
+
if (others.length > 0) {
|
|
2739
|
+
console.warn(
|
|
2740
|
+
`[BundleManager] ${pluginId}: not registering tools from export(s) ${others.join(", ")} \u2014 they are not this plugin's, and only their own manifest can grant them`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
if (!own) return;
|
|
2744
|
+
const ungranted = [
|
|
2745
|
+
...(Array.isArray(own.stateDomains) ? own.stateDomains : []).filter((d) => !stateDomains?.includes(d)),
|
|
2746
|
+
...Object.keys(own.capabilities || {}).filter((k) => own.capabilities[k] === true && !capabilities?.[k])
|
|
2747
|
+
];
|
|
2748
|
+
if (ungranted.length > 0) {
|
|
2749
|
+
console.warn(
|
|
2750
|
+
`[BundleManager] ${pluginId}: bundle declares ${ungranted.join(", ")} but the manifest does not grant it \u2014 denied`
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
try {
|
|
2754
|
+
registerDeviceTools(pluginId, own.tools, capabilities, stateDomains);
|
|
2755
|
+
this.registeredToolPluginIds.add(pluginId);
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
console.warn(
|
|
2758
|
+
`[BundleManager] tool registration failed for ${pluginId}:`,
|
|
2759
|
+
err instanceof Error ? err.message : String(err)
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Record the trusted declaration on this tenant's disk-cache entry for the
|
|
2765
|
+
* bytes RUNNING — the recovery path (tools/loader loadFromDiskCache) has no
|
|
2766
|
+
* manifest and re-registers with exactly this. Only onto an entry this tenant
|
|
2767
|
+
* owns whose hash is the running hash: the grant was issued for these bytes,
|
|
2768
|
+
* and those are the bytes recovery will evaluate. Best-effort — a lost write
|
|
2769
|
+
* leaves no `trusted`, which recovery treats as fail closed.
|
|
2770
|
+
*/
|
|
2771
|
+
async recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch) {
|
|
2772
|
+
const key = JSON.stringify(declaration);
|
|
2773
|
+
if (this.recordedDeclarations.get(bundleUrl) === key) return;
|
|
2774
|
+
try {
|
|
2775
|
+
const storage = getBundleStorage();
|
|
2776
|
+
const index = await loadCacheIndex(storage);
|
|
2777
|
+
const entry = index[bundleUrl];
|
|
2778
|
+
const running = this.provenance.get(bundleUrl)?.hash;
|
|
2779
|
+
if (!entry || entry.tenant !== tenant || !running || entry.bundleHash?.toLowerCase() !== running) return;
|
|
2780
|
+
if (this.isStale(epoch, tenant)) return;
|
|
2781
|
+
if (JSON.stringify(entry.trusted) !== key) {
|
|
2782
|
+
index[bundleUrl] = { ...entry, trusted: declaration };
|
|
2783
|
+
await storage.saveIndex(index);
|
|
2583
2784
|
}
|
|
2785
|
+
this.recordedDeclarations.set(bundleUrl, key);
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
console.warn("[BundleManager] could not record trusted declaration:", err);
|
|
2584
2788
|
}
|
|
2585
2789
|
}
|
|
2586
2790
|
/**
|
|
@@ -2663,29 +2867,26 @@ var BundleManager = class {
|
|
|
2663
2867
|
return materialized;
|
|
2664
2868
|
}
|
|
2665
2869
|
// ── Private: component extraction ───────────────────────────
|
|
2870
|
+
/** Find a plugin component in a loaded module. */
|
|
2871
|
+
getComponentFromMemory(pluginId, bundleUrl) {
|
|
2872
|
+
return this.findPluginExport(this.modules.get(bundleUrl), pluginId, hasComponent);
|
|
2873
|
+
}
|
|
2666
2874
|
/**
|
|
2667
|
-
* Find
|
|
2668
|
-
*
|
|
2875
|
+
* Find THIS plugin's export in a loaded module — the one lookup both the
|
|
2876
|
+
* component and the device tools use, so a plugin's UI and its tools always
|
|
2877
|
+
* come from the same export. `accept` says what the caller needs from it.
|
|
2878
|
+
* Handles: direct key match, PascalCase key, default export, multi-plugin .id match.
|
|
2669
2879
|
*/
|
|
2670
|
-
|
|
2671
|
-
const mod = this.modules.get(bundleUrl);
|
|
2880
|
+
findPluginExport(mod, pluginId, accept) {
|
|
2672
2881
|
if (!mod) return null;
|
|
2673
2882
|
const shortName = pluginId.includes(":") ? pluginId.split(":").pop() : pluginId;
|
|
2674
|
-
if (mod[pluginId]
|
|
2675
|
-
return mod[pluginId];
|
|
2676
|
-
}
|
|
2883
|
+
if (accept(mod[pluginId])) return mod[pluginId];
|
|
2677
2884
|
const pascal = shortName.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
2678
|
-
if (mod[pascal]
|
|
2679
|
-
|
|
2680
|
-
}
|
|
2681
|
-
if (mod.default && typeof mod.default === "object" && typeof mod.default.Component === "function") {
|
|
2682
|
-
return mod.default;
|
|
2683
|
-
}
|
|
2885
|
+
if (accept(mod[pascal])) return mod[pascal];
|
|
2886
|
+
if (accept(mod.default)) return mod.default;
|
|
2684
2887
|
for (const key of Object.keys(mod)) {
|
|
2685
2888
|
const val = mod[key];
|
|
2686
|
-
if (val &&
|
|
2687
|
-
return val;
|
|
2688
|
-
}
|
|
2889
|
+
if (val && (val.id === pluginId || val.id === shortName) && accept(val)) return val;
|
|
2689
2890
|
}
|
|
2690
2891
|
return null;
|
|
2691
2892
|
}
|
|
@@ -2700,6 +2901,107 @@ var BundleManager = class {
|
|
|
2700
2901
|
};
|
|
2701
2902
|
var bundleManager = new BundleManager();
|
|
2702
2903
|
|
|
2904
|
+
// src/voiceHost.ts
|
|
2905
|
+
var _provider2 = null;
|
|
2906
|
+
function setVoiceProvider(provider) {
|
|
2907
|
+
_provider2 = provider || null;
|
|
2908
|
+
}
|
|
2909
|
+
function implementedVoice() {
|
|
2910
|
+
return !!_provider2;
|
|
2911
|
+
}
|
|
2912
|
+
function voiceDenial(why) {
|
|
2913
|
+
const err = new Error(`voice: ${why}`);
|
|
2914
|
+
err.code = "RESTRICTED";
|
|
2915
|
+
return err;
|
|
2916
|
+
}
|
|
2917
|
+
function deniedVoice(why) {
|
|
2918
|
+
const fail = () => {
|
|
2919
|
+
throw voiceDenial(why);
|
|
2920
|
+
};
|
|
2921
|
+
return {
|
|
2922
|
+
async start() {
|
|
2923
|
+
return fail();
|
|
2924
|
+
},
|
|
2925
|
+
stop() {
|
|
2926
|
+
fail();
|
|
2927
|
+
},
|
|
2928
|
+
setMuted() {
|
|
2929
|
+
fail();
|
|
2930
|
+
},
|
|
2931
|
+
prompt() {
|
|
2932
|
+
fail();
|
|
2933
|
+
},
|
|
2934
|
+
observe() {
|
|
2935
|
+
fail();
|
|
2936
|
+
},
|
|
2937
|
+
getSnapshot() {
|
|
2938
|
+
return {
|
|
2939
|
+
state: "error",
|
|
2940
|
+
sessionId: null,
|
|
2941
|
+
muted: false,
|
|
2942
|
+
speaking: false,
|
|
2943
|
+
listening: false,
|
|
2944
|
+
userSpeaking: false,
|
|
2945
|
+
lastUserSpeechEndedAt: null,
|
|
2946
|
+
lastDeviceTool: null,
|
|
2947
|
+
authorizedSessionId: null,
|
|
2948
|
+
lastError: why
|
|
2949
|
+
};
|
|
2950
|
+
},
|
|
2951
|
+
subscribe() {
|
|
2952
|
+
return fail();
|
|
2953
|
+
},
|
|
2954
|
+
transcript() {
|
|
2955
|
+
return [];
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
function scopedVoice(provider, pluginId) {
|
|
2960
|
+
return {
|
|
2961
|
+
start: (opts) => provider.start(opts, pluginId),
|
|
2962
|
+
stop: () => provider.stop(pluginId),
|
|
2963
|
+
setMuted: (muted) => provider.setMuted(muted, pluginId),
|
|
2964
|
+
prompt: (input) => provider.prompt(input, pluginId),
|
|
2965
|
+
observe: (text) => provider.observe(text, pluginId),
|
|
2966
|
+
getSnapshot: () => provider.getSnapshot(pluginId),
|
|
2967
|
+
subscribe: (cb) => provider.subscribe(cb, pluginId),
|
|
2968
|
+
transcript: () => provider.transcript(pluginId)
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
function gateVoice(declared, pluginId) {
|
|
2972
|
+
if (!declared) {
|
|
2973
|
+
return deniedVoice(
|
|
2974
|
+
"not declared in this plugin's capabilities. Add `capabilities: { voice: true }` to use it."
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2977
|
+
if (!pluginId) {
|
|
2978
|
+
return deniedVoice(
|
|
2979
|
+
"no plugin identity \u2014 voice fails closed without a caller to scope the session to."
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
return _provider2 ? scopedVoice(_provider2, pluginId) : deniedVoice(
|
|
2983
|
+
"no voice session in this build \u2014 the app has not mounted a VoiceSessionHost / called setVoiceProvider."
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// src/pluginRuntime.ts
|
|
2988
|
+
function mergeCaps(decl) {
|
|
2989
|
+
if (!decl) return void 0;
|
|
2990
|
+
return { ...decl.native, ...decl.capabilities };
|
|
2991
|
+
}
|
|
2992
|
+
function resolvePluginGrants(src) {
|
|
2993
|
+
const trusted = src.manifest ?? src.plugin ?? null;
|
|
2994
|
+
const stateDomains = trusted?.stateDomains ? [...trusted.stateDomains] : void 0;
|
|
2995
|
+
return { capabilities: mergeCaps(trusted), stateDomains };
|
|
2996
|
+
}
|
|
2997
|
+
function resolvePluginRuntimeProps(src) {
|
|
2998
|
+
const { capabilities, stateDomains } = resolvePluginGrants(src);
|
|
2999
|
+
const native = gatePluginNative(getPluginNative(), capabilities, src.pluginId);
|
|
3000
|
+
const deviceState = gateDeviceState(stateDomains);
|
|
3001
|
+
const voice = gateVoice(!!capabilities?.voice, src.pluginId);
|
|
3002
|
+
return { device: native, native, deviceState, voice };
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2703
3005
|
// src/tools/loader.ts
|
|
2704
3006
|
import { getTenant as getTenant3 } from "@ateam-ai-mobile/core";
|
|
2705
3007
|
var inflight = null;
|
|
@@ -2740,7 +3042,8 @@ async function ensureDeviceToolsLoaded({ force = false } = {}) {
|
|
|
2740
3042
|
m.id,
|
|
2741
3043
|
bundleUrl,
|
|
2742
3044
|
m.version || "1.0.0",
|
|
2743
|
-
getNativeBundleHash(m) || void 0
|
|
3045
|
+
getNativeBundleHash(m) || void 0,
|
|
3046
|
+
resolvePluginGrants({ pluginId: m.id, manifest: m })
|
|
2744
3047
|
);
|
|
2745
3048
|
} catch (err) {
|
|
2746
3049
|
console.warn(`[deviceTools] ${m.id} failed to load:`, err?.message || String(err));
|
|
@@ -2759,7 +3062,7 @@ async function ensureDeviceToolsLoaded({ force = false } = {}) {
|
|
|
2759
3062
|
}
|
|
2760
3063
|
async function loadFromDiskCache() {
|
|
2761
3064
|
try {
|
|
2762
|
-
const index = await getBundleStorage()
|
|
3065
|
+
const index = await loadCacheIndex(getBundleStorage());
|
|
2763
3066
|
const entries = Object.values(index || {});
|
|
2764
3067
|
if (entries.length === 0) return [];
|
|
2765
3068
|
const tenant = getTenant3();
|
|
@@ -2772,10 +3075,16 @@ async function loadFromDiskCache() {
|
|
|
2772
3075
|
if (skipped > 0) {
|
|
2773
3076
|
console.log(`[deviceTools] disk-cache recovery: ${mine.length} for "${tenant}", skipped ${skipped} not owned by it`);
|
|
2774
3077
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
3078
|
+
const declared = mine.filter((e) => !!e.trusted?.pluginId);
|
|
3079
|
+
if (declared.length < mine.length) {
|
|
3080
|
+
console.log(
|
|
3081
|
+
`[deviceTools] disk-cache recovery: skipped ${mine.length - declared.length} with no recorded manifest grant (fail closed)`
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
if (declared.length === 0) return [];
|
|
3085
|
+
await Promise.all(declared.map(async (e) => {
|
|
2777
3086
|
try {
|
|
2778
|
-
await bundleManager.loadTools(
|
|
3087
|
+
await bundleManager.loadTools(e.trusted.pluginId, e.bundleUrl, e.version, e.bundleHash, e.trusted);
|
|
2779
3088
|
} catch {
|
|
2780
3089
|
}
|
|
2781
3090
|
}));
|
|
@@ -2790,11 +3099,14 @@ function resetDeviceToolLoader() {
|
|
|
2790
3099
|
}
|
|
2791
3100
|
|
|
2792
3101
|
export {
|
|
2793
|
-
__export,
|
|
2794
3102
|
register,
|
|
2795
3103
|
registerTools,
|
|
2796
3104
|
registerService,
|
|
2797
3105
|
PLATFORM_VOICE_CALLER,
|
|
3106
|
+
isActionable,
|
|
3107
|
+
neverObserved,
|
|
3108
|
+
explainCapability,
|
|
3109
|
+
device_state_exports,
|
|
2798
3110
|
View,
|
|
2799
3111
|
Text,
|
|
2800
3112
|
ScrollView,
|
|
@@ -2839,6 +3151,11 @@ export {
|
|
|
2839
3151
|
setDeviceStateProvider,
|
|
2840
3152
|
gateDeviceState,
|
|
2841
3153
|
implementedStateDomains,
|
|
3154
|
+
setVoiceProvider,
|
|
3155
|
+
implementedVoice,
|
|
3156
|
+
gateVoice,
|
|
3157
|
+
resolvePluginGrants,
|
|
3158
|
+
resolvePluginRuntimeProps,
|
|
2842
3159
|
useNativeBridge,
|
|
2843
3160
|
createNativeBridge,
|
|
2844
3161
|
setDeviceToolContextProvider,
|
|
@@ -2848,6 +3165,7 @@ export {
|
|
|
2848
3165
|
listDeviceTools,
|
|
2849
3166
|
callDeviceTool,
|
|
2850
3167
|
clearDeviceTools,
|
|
3168
|
+
pluginBundleUrl,
|
|
2851
3169
|
supportsNative,
|
|
2852
3170
|
getNativeComponentId,
|
|
2853
3171
|
getNativeBundleUrl,
|
|
@@ -2870,4 +3188,4 @@ export {
|
|
|
2870
3188
|
ensureDeviceToolsLoaded,
|
|
2871
3189
|
resetDeviceToolLoader
|
|
2872
3190
|
};
|
|
2873
|
-
//# sourceMappingURL=chunk-
|
|
3191
|
+
//# sourceMappingURL=chunk-SICFDA5K.mjs.map
|