@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/index.js CHANGED
@@ -75,6 +75,66 @@ var init_voice = __esm({
75
75
  }
76
76
  });
77
77
 
78
+ // src/sdk/device-state.ts
79
+ var device_state_exports = {};
80
+ __export(device_state_exports, {
81
+ DeviceBridgeError: () => DeviceBridgeError,
82
+ explainCapability: () => explainCapability,
83
+ isActionable: () => isActionable,
84
+ neverObserved: () => neverObserved
85
+ });
86
+ function isActionable(quality, budget) {
87
+ const { freshness, confidence } = quality;
88
+ if (freshness.state === "unknown" || freshness.state === "stale") return false;
89
+ if (freshness.ageMs === null || freshness.ageMs > budget.maxAgeMs) return false;
90
+ if (budget.minConfidence) {
91
+ const have = CONFIDENCE_ORDER[confidence?.level ?? "unknown"];
92
+ if (have < CONFIDENCE_ORDER[budget.minConfidence]) return false;
93
+ }
94
+ return true;
95
+ }
96
+ function neverObserved() {
97
+ return { state: "unknown", observedAt: null, ageMs: null };
98
+ }
99
+ function explainCapability(status) {
100
+ if (status.support === "unsupported") return "this device does not have it";
101
+ if (status.permission === "denied") return "permission was denied";
102
+ if (status.permission === "restricted") return "permission is restricted";
103
+ if (status.permission === "notDetermined") return "permission has not been asked for yet";
104
+ if (status.permission === "limited") return "only limited access was granted";
105
+ if (status.restriction === "policyRestricted") return "this plugin did not declare it";
106
+ if (status.restriction === "backgroundRestricted") return "the app is in the background";
107
+ if (status.restriction === "lowPowerRestricted") return "the device is in low-power mode";
108
+ if (status.availability === "busy") return "it is in use by something else";
109
+ if (status.availability === "temporarilyUnavailable") return "it is temporarily unavailable";
110
+ if (status.availability === "unavailable") return "it is unavailable";
111
+ if (status.support === "unknown" || status.availability === "unknown") {
112
+ return "its state could not be determined";
113
+ }
114
+ return null;
115
+ }
116
+ var DeviceBridgeError, CONFIDENCE_ORDER;
117
+ var init_device_state = __esm({
118
+ "src/sdk/device-state.ts"() {
119
+ "use strict";
120
+ DeviceBridgeError = class extends Error {
121
+ constructor(code, message, opts) {
122
+ super(message);
123
+ this.name = "DeviceBridgeError";
124
+ this.code = code;
125
+ this.domain = opts?.domain;
126
+ this.recoverable = opts?.recoverable;
127
+ }
128
+ };
129
+ CONFIDENCE_ORDER = {
130
+ unknown: 0,
131
+ low: 1,
132
+ medium: 2,
133
+ high: 3
134
+ };
135
+ }
136
+ });
137
+
78
138
  // src/sdk/primitives.ts
79
139
  var import_react_native;
80
140
  var init_primitives = __esm({
@@ -395,7 +455,8 @@ function gatePluginNative(native, declared, pluginId) {
395
455
  // every method refuses by name. Both are better than the `undefined` this
396
456
  // used to be, which crashed with "cannot read properties of undefined".
397
457
  bluetooth: caps.bluetooth ? native.bluetooth : blocked("bluetooth"),
398
- vision: caps.vision ? native.vision : blocked("vision")
458
+ vision: caps.vision ? native.vision : blocked("vision"),
459
+ maps: caps.maps ? native.maps : blocked("maps")
399
460
  // What this plugin DECLARED, carried on the object it already receives.
400
461
  //
401
462
  // It is how a COMPONENT can be capability-gated: <CameraPreview> is handed
@@ -455,7 +516,8 @@ var init_host = __esm({
455
516
  scanner: unavailable("scanner"),
456
517
  nfc: unavailable("nfc"),
457
518
  bluetooth: unavailable("bluetooth"),
458
- vision: unavailable("vision")
519
+ vision: unavailable("vision"),
520
+ maps: unavailable("maps")
459
521
  };
460
522
  _pluginNative = noopNative;
461
523
  _cameraPreview = null;
@@ -664,6 +726,8 @@ __export(sdk_exports, {
664
726
  TextInput: () => import_react_native.TextInput,
665
727
  TouchableOpacity: () => import_react_native.TouchableOpacity,
666
728
  View: () => import_react_native.View,
729
+ explainCapability: () => explainCapability,
730
+ isActionable: () => isActionable,
667
731
  register: () => register,
668
732
  registerService: () => registerService,
669
733
  registerTools: () => registerTools,
@@ -678,6 +742,7 @@ var init_sdk = __esm({
678
742
  init_registration();
679
743
  init_registration();
680
744
  init_voice();
745
+ init_device_state();
681
746
  init_primitives();
682
747
  init_components();
683
748
  init_CameraPreview();
@@ -814,6 +879,19 @@ function gateDeviceState(declared) {
814
879
  }
815
880
  return out;
816
881
  }
882
+ function withoutSubscribe(state) {
883
+ if (!state) return state;
884
+ const out = {};
885
+ for (const domain of ALL_DOMAINS) {
886
+ const d = state[domain];
887
+ out[domain] = d && Object.assign(Object.create(d), {
888
+ subscribe() {
889
+ 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.");
890
+ }
891
+ });
892
+ }
893
+ return out;
894
+ }
817
895
  function implementedStateDomains() {
818
896
  return ALL_DOMAINS.filter((d) => !!_provider[d]);
819
897
  }
@@ -839,6 +917,121 @@ var init_deviceStateHost = __esm({
839
917
  }
840
918
  });
841
919
 
920
+ // src/voiceHost.ts
921
+ function setVoiceProvider(provider) {
922
+ _provider2 = provider || null;
923
+ }
924
+ function implementedVoice() {
925
+ return !!_provider2;
926
+ }
927
+ function voiceDenial(why) {
928
+ const err = new Error(`voice: ${why}`);
929
+ err.code = "RESTRICTED";
930
+ return err;
931
+ }
932
+ function deniedVoice(why) {
933
+ const fail = () => {
934
+ throw voiceDenial(why);
935
+ };
936
+ return {
937
+ async start() {
938
+ return fail();
939
+ },
940
+ stop() {
941
+ fail();
942
+ },
943
+ setMuted() {
944
+ fail();
945
+ },
946
+ prompt() {
947
+ fail();
948
+ },
949
+ observe() {
950
+ fail();
951
+ },
952
+ getSnapshot() {
953
+ return {
954
+ state: "error",
955
+ sessionId: null,
956
+ muted: false,
957
+ speaking: false,
958
+ listening: false,
959
+ userSpeaking: false,
960
+ lastUserSpeechEndedAt: null,
961
+ lastDeviceTool: null,
962
+ authorizedSessionId: null,
963
+ lastError: why
964
+ };
965
+ },
966
+ subscribe() {
967
+ return fail();
968
+ },
969
+ transcript() {
970
+ return [];
971
+ }
972
+ };
973
+ }
974
+ function scopedVoice(provider, pluginId) {
975
+ return {
976
+ start: (opts) => provider.start(opts, pluginId),
977
+ stop: () => provider.stop(pluginId),
978
+ setMuted: (muted) => provider.setMuted(muted, pluginId),
979
+ prompt: (input) => provider.prompt(input, pluginId),
980
+ observe: (text) => provider.observe(text, pluginId),
981
+ getSnapshot: () => provider.getSnapshot(pluginId),
982
+ subscribe: (cb) => provider.subscribe(cb, pluginId),
983
+ transcript: () => provider.transcript(pluginId)
984
+ };
985
+ }
986
+ function gateVoice(declared, pluginId) {
987
+ if (!declared) {
988
+ return deniedVoice(
989
+ "not declared in this plugin's capabilities. Add `capabilities: { voice: true }` to use it."
990
+ );
991
+ }
992
+ if (!pluginId) {
993
+ return deniedVoice(
994
+ "no plugin identity \u2014 voice fails closed without a caller to scope the session to."
995
+ );
996
+ }
997
+ return _provider2 ? scopedVoice(_provider2, pluginId) : deniedVoice(
998
+ "no voice session in this build \u2014 the app has not mounted a VoiceSessionHost / called setVoiceProvider."
999
+ );
1000
+ }
1001
+ var _provider2;
1002
+ var init_voiceHost = __esm({
1003
+ "src/voiceHost.ts"() {
1004
+ "use strict";
1005
+ _provider2 = null;
1006
+ }
1007
+ });
1008
+
1009
+ // src/pluginRuntime.ts
1010
+ function mergeCaps(decl) {
1011
+ if (!decl) return void 0;
1012
+ return { ...decl.native, ...decl.capabilities };
1013
+ }
1014
+ function resolvePluginGrants(src) {
1015
+ const trusted = src.manifest ?? src.plugin ?? null;
1016
+ const stateDomains = trusted?.stateDomains ? [...trusted.stateDomains] : void 0;
1017
+ return { capabilities: mergeCaps(trusted), stateDomains };
1018
+ }
1019
+ function resolvePluginRuntimeProps(src) {
1020
+ const { capabilities, stateDomains } = resolvePluginGrants(src);
1021
+ const native = gatePluginNative(getPluginNative(), capabilities, src.pluginId);
1022
+ const deviceState = gateDeviceState(stateDomains);
1023
+ const voice = gateVoice(!!capabilities?.voice, src.pluginId);
1024
+ return { device: native, native, deviceState, voice };
1025
+ }
1026
+ var init_pluginRuntime = __esm({
1027
+ "src/pluginRuntime.ts"() {
1028
+ "use strict";
1029
+ init_host();
1030
+ init_deviceStateHost();
1031
+ init_voiceHost();
1032
+ }
1033
+ });
1034
+
842
1035
  // src/bridge/native.ts
843
1036
  function buildContext(pluginId, connectorId) {
844
1037
  const user = (0, import_core2.getUser)();
@@ -1086,8 +1279,9 @@ function unregisterDeviceTools(pluginId) {
1086
1279
  if (entry.pluginId === pluginId) registry.delete(name);
1087
1280
  }
1088
1281
  }
1089
- function hasDeviceTool(name) {
1090
- return registry.has(name);
1282
+ function hasDeviceTool(name, pluginId) {
1283
+ const entry = registry.get(name);
1284
+ return !!entry && (pluginId === void 0 || entry.pluginId === pluginId);
1091
1285
  }
1092
1286
  function listDeviceTools() {
1093
1287
  return Array.from(registry.entries()).map(([name, { pluginId, tool }]) => ({
@@ -1106,8 +1300,11 @@ async function callDeviceTool(name, args = {}, opts = {}) {
1106
1300
  let timer;
1107
1301
  try {
1108
1302
  const build = contextProvider || defaultContext;
1303
+ const built = build(entry.pluginId, entry.capabilities, entry.stateDomains);
1109
1304
  const ctx = {
1110
- ...build(entry.pluginId, entry.capabilities, entry.stateDomains),
1305
+ ...built,
1306
+ // Whoever built the context, a call cannot own a subscription.
1307
+ deviceState: withoutSubscribe(built.deviceState),
1111
1308
  signal: opts.signal
1112
1309
  };
1113
1310
  const result = await Promise.race([
@@ -1144,6 +1341,26 @@ var init_registry = __esm({
1144
1341
  }
1145
1342
  });
1146
1343
 
1344
+ // src/bundles/bundleUrl.ts
1345
+ function pluginBundleUrl(pluginId) {
1346
+ return `/api/ui-plugins/${encodeURIComponent(pluginId)}/bundle.js`;
1347
+ }
1348
+ function isPluginBundleUrl(url, pluginId) {
1349
+ if (typeof url !== "string") return false;
1350
+ const canonical = pluginBundleUrl(pluginId);
1351
+ if (url === canonical) return true;
1352
+ try {
1353
+ return decodeURIComponent(url) === decodeURIComponent(canonical);
1354
+ } catch {
1355
+ return false;
1356
+ }
1357
+ }
1358
+ var init_bundleUrl = __esm({
1359
+ "src/bundles/bundleUrl.ts"() {
1360
+ "use strict";
1361
+ }
1362
+ });
1363
+
1147
1364
  // src/api/plugins.ts
1148
1365
  function supportsNative(manifest) {
1149
1366
  const mode = manifest.render?.mode;
@@ -1155,12 +1372,9 @@ function getNativeComponentId(manifest) {
1155
1372
  return null;
1156
1373
  }
1157
1374
  function getNativeBundleUrl(manifest) {
1158
- const direct = manifest.bundleUrl;
1159
- if (direct) return direct;
1160
1375
  const render = manifest.render;
1161
- if (render.mode === "react-native") return render.bundleUrl || null;
1162
- if (render.mode === "adaptive") return render.reactNative?.bundleUrl || null;
1163
- return null;
1376
+ const declared = manifest.bundleUrl || (render?.mode === "react-native" ? render.bundleUrl : null) || (render?.mode === "adaptive" ? render.reactNative?.bundleUrl : null);
1377
+ return declared && manifest.id ? pluginBundleUrl(manifest.id) : null;
1164
1378
  }
1165
1379
  function getNativeBundleHash(manifest) {
1166
1380
  const direct = manifest.bundleHash;
@@ -1250,6 +1464,7 @@ var init_plugins = __esm({
1250
1464
  "use strict";
1251
1465
  import_core3 = require("@ateam-ai-mobile/core");
1252
1466
  import_core4 = require("@ateam-ai-mobile/core");
1467
+ init_bundleUrl();
1253
1468
  }
1254
1469
  });
1255
1470
 
@@ -2078,6 +2293,20 @@ var init_SurfaceDispatcher = __esm({
2078
2293
  }
2079
2294
  });
2080
2295
 
2296
+ // src/bundles/bundleModuleNames.ts
2297
+ function sdkModuleRefusal(name) {
2298
+ if (name === PLUGIN_SDK_MODULE) return null;
2299
+ if (!name.startsWith("@ateam-ai-mobile/") && !name.startsWith("@adas/")) return null;
2300
+ 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.`;
2301
+ }
2302
+ var PLUGIN_SDK_MODULE;
2303
+ var init_bundleModuleNames = __esm({
2304
+ "src/bundles/bundleModuleNames.ts"() {
2305
+ "use strict";
2306
+ PLUGIN_SDK_MODULE = "@adas/plugin-sdk";
2307
+ }
2308
+ });
2309
+
2081
2310
  // src/utils/sha256.ts
2082
2311
  function sha256Hex(message) {
2083
2312
  const utf8 = unescape(encodeURIComponent(message));
@@ -2244,6 +2473,22 @@ function selectOwnedCacheEntries(entries, tenant) {
2244
2473
  if (!tenant) return [];
2245
2474
  return (entries || []).filter((e) => !!e && e.tenant === tenant);
2246
2475
  }
2476
+ async function loadCacheIndex(storage = getBundleStorage()) {
2477
+ const index = await storage.loadIndex() || {};
2478
+ for (const [key, entry] of Object.entries(index)) {
2479
+ const pluginId = entry?.trusted?.pluginId;
2480
+ if (!pluginId) continue;
2481
+ const canonical = pluginBundleUrl(pluginId);
2482
+ if (key === canonical) continue;
2483
+ if (!isPluginBundleUrl(key, pluginId) || !isPluginBundleUrl(entry.bundleUrl, pluginId)) continue;
2484
+ delete index[key];
2485
+ const held = index[canonical];
2486
+ if (!held || (entry.timestamp ?? 0) > (held.timestamp ?? 0)) {
2487
+ index[canonical] = { ...entry, bundleUrl: canonical };
2488
+ }
2489
+ }
2490
+ return index;
2491
+ }
2247
2492
  function setBundleStorage(storage) {
2248
2493
  _storage = storage;
2249
2494
  }
@@ -2254,6 +2499,7 @@ var MemoryBundleStorage, _storage;
2254
2499
  var init_storage = __esm({
2255
2500
  "src/bundles/storage.ts"() {
2256
2501
  "use strict";
2502
+ init_bundleUrl();
2257
2503
  MemoryBundleStorage = class {
2258
2504
  constructor() {
2259
2505
  this.index = {};
@@ -2286,7 +2532,10 @@ var init_storage = __esm({
2286
2532
  });
2287
2533
 
2288
2534
  // src/bundles/bundleManager.ts
2289
- var import_react9, import_react10, ReactNative, import_core9, ReactShim, PluginSDKShim, requireShim, CACHE_TTL, BundleManager, bundleManager;
2535
+ function bundleKey(pluginId, bundleUrl) {
2536
+ return isPluginBundleUrl(bundleUrl, pluginId) ? pluginBundleUrl(pluginId) : bundleUrl;
2537
+ }
2538
+ var import_react9, import_react10, ReactNative, import_core9, ReactShim, PluginSDKShim, requireShim, CACHE_TTL, hasComponent, hasTools, BundleManager, bundleManager;
2290
2539
  var init_bundleManager = __esm({
2291
2540
  "src/bundles/bundleManager.ts"() {
2292
2541
  "use strict";
@@ -2301,9 +2550,11 @@ var init_bundleManager = __esm({
2301
2550
  init_store();
2302
2551
  init_SurfaceDispatcher();
2303
2552
  init_host();
2553
+ init_bundleModuleNames();
2304
2554
  init_sha256();
2305
2555
  init_integrity();
2306
2556
  init_storage();
2557
+ init_bundleUrl();
2307
2558
  init_registry();
2308
2559
  ReactShim = {
2309
2560
  __esModule: true,
@@ -2350,13 +2601,17 @@ var init_bundleManager = __esm({
2350
2601
  requireShim = (name) => {
2351
2602
  if (name === "react") return ReactShim;
2352
2603
  if (name === "react-native") return ReactNative;
2353
- if (name === "@adas/plugin-sdk") return PluginSDKShim;
2604
+ if (name === PLUGIN_SDK_MODULE) return PluginSDKShim;
2605
+ const refusal = sdkModuleRefusal(name);
2606
+ if (refusal) throw new Error(`[BundleManager] ${refusal}`);
2354
2607
  const hostModule = resolveBundleModule(name);
2355
2608
  if (hostModule !== void 0) return hostModule;
2356
2609
  console.warn(`[BundleManager] Unknown module: ${name} \u2014 register it via registerBundleModules()`);
2357
2610
  return {};
2358
2611
  };
2359
2612
  CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
2613
+ hasComponent = (val) => !!val && typeof val === "object" && typeof val.Component === "function";
2614
+ hasTools = (val) => !!val && typeof val === "object" && Array.isArray(val.tools) && val.tools.length > 0;
2360
2615
  BundleManager = class {
2361
2616
  constructor() {
2362
2617
  /** In-memory module cache: bundleUrl → evaluated module.exports. Holds ONE
@@ -2387,6 +2642,9 @@ var init_bundleManager = __esm({
2387
2642
  * of the previous tenant's tools.
2388
2643
  */
2389
2644
  this.registeredToolPluginIds = /* @__PURE__ */ new Set();
2645
+ /** bundleUrl → the trusted declaration last recorded on its disk-cache entry
2646
+ * this session, so a memory hit with unchanged grants skips the index I/O. */
2647
+ this.recordedDeclarations = /* @__PURE__ */ new Map();
2390
2648
  /**
2391
2649
  * Enforce the in-memory tenant boundary. Call at the top of every public load
2392
2650
  * path, BEFORE any cache read.
@@ -2432,6 +2690,7 @@ var init_bundleManager = __esm({
2432
2690
  }
2433
2691
  }
2434
2692
  this.registeredToolPluginIds.clear();
2693
+ this.recordedDeclarations.clear();
2435
2694
  this.modules.clear();
2436
2695
  this.provenance.clear();
2437
2696
  this.inflight.clear();
@@ -2443,13 +2702,15 @@ var init_bundleManager = __esm({
2443
2702
  isStale(epoch, tenant) {
2444
2703
  return this.epoch !== epoch || (0, import_core9.getTenant)() !== tenant;
2445
2704
  }
2446
- /** Flip an existing provenance record to source:'memory' — a later serve of an
2447
- * already-evaluated bundle came from the in-memory cache, not a fresh
2448
- * download/disk read. No-op if nothing was recorded yet. */
2449
- markMemoryHit(bundleUrl) {
2705
+ /** Record a memory serve: the running bytes are the already-evaluated module,
2706
+ * and `expected` is refreshed from the manifest hash the caller JUST passed —
2707
+ * so after a redeploy the record reads hash !== expected (STALE) instead of
2708
+ * the old in-sync pair. The ONE place both memory paths (load()'s early return
2709
+ * and loadModule's) record reuse. No-op if nothing was recorded yet. */
2710
+ markMemoryHit(bundleUrl, expectedHash) {
2450
2711
  const prev = this.provenance.get(bundleUrl);
2451
- if (prev && prev.source !== "memory") {
2452
- this.provenance.set(bundleUrl, { ...prev, source: "memory" });
2712
+ if (prev) {
2713
+ this.provenance.set(bundleUrl, { ...prev, source: "memory", expected: expectedHash?.toLowerCase() ?? prev.expected });
2453
2714
  }
2454
2715
  }
2455
2716
  // ── Public API ──────────────────────────────────────────────
@@ -2461,13 +2722,22 @@ var init_bundleManager = __esm({
2461
2722
  * provided for any bundle the user has not previously verified — without
2462
2723
  * it, a tampered bundle is `eval`'d as JS with full RN runtime + every
2463
2724
  * native capability the manifest declares.
2725
+ *
2726
+ * `grants` is what the MANIFEST grants this plugin (resolvePluginGrants) — the
2727
+ * only authority its device tools are registered under. Omitted (e.g. a
2728
+ * prewarm from the slim plugin list) → the bundle is loaded but its tools are
2729
+ * NOT registered: without the manifest there is nothing trusted to gate them
2730
+ * on, and registering with nothing would downgrade a registration the
2731
+ * manifest-driven tool loader already made.
2464
2732
  */
2465
- async load(pluginId, bundleUrl, version, expectedHash) {
2733
+ async load(pluginId, bundleUrl, version, expectedHash, grants) {
2734
+ bundleUrl = bundleKey(pluginId, bundleUrl);
2466
2735
  const tenant = this.requireTenant();
2467
2736
  const epoch = this.epoch;
2468
2737
  const cached = this.getComponentFromMemory(pluginId, bundleUrl);
2469
2738
  if (cached) {
2470
- this.markMemoryHit(bundleUrl);
2739
+ this.markMemoryHit(bundleUrl, expectedHash);
2740
+ if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2471
2741
  return cached;
2472
2742
  }
2473
2743
  let modulePromise = this.inflight.get(bundleUrl);
@@ -2484,6 +2754,7 @@ var init_bundleManager = __esm({
2484
2754
  if (this.isStale(epoch, tenant)) {
2485
2755
  throw new Error("[BundleManager] tenant changed while loading \u2014 discarding (fail closed)");
2486
2756
  }
2757
+ if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2487
2758
  const component = this.getComponentFromMemory(pluginId, bundleUrl);
2488
2759
  if (!component) {
2489
2760
  throw new Error(
@@ -2498,8 +2769,15 @@ var init_bundleManager = __esm({
2498
2769
  * Same download, same integrity check, same evaluation as `load()`; the only
2499
2770
  * difference is that a bundle exporting tools and no `Component` is a success
2500
2771
  * here rather than "no component found". Returns the tool names registered.
2772
+ *
2773
+ * `grants` is REQUIRED: the tools are registered under `pluginId` with exactly
2774
+ * what the manifest grants (see load()), never what the bundle claims.
2501
2775
  */
2502
- async loadTools(pluginId, bundleUrl, version, expectedHash) {
2776
+ async loadTools(pluginId, bundleUrl, version, expectedHash, grants) {
2777
+ if (!grants) {
2778
+ throw new Error(`[BundleManager] loadTools(${pluginId}) needs the manifest's grants (resolvePluginGrants) \u2014 refusing to register tools on the bundle's own claim`);
2779
+ }
2780
+ bundleUrl = bundleKey(pluginId, bundleUrl);
2503
2781
  const tenant = this.requireTenant();
2504
2782
  const epoch = this.epoch;
2505
2783
  let modulePromise = this.inflight.get(bundleUrl);
@@ -2516,6 +2794,7 @@ var init_bundleManager = __esm({
2516
2794
  if (this.isStale(epoch, tenant)) {
2517
2795
  throw new Error("[BundleManager] tenant changed while loading tools \u2014 discarding (fail closed)");
2518
2796
  }
2797
+ await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2519
2798
  const names = listDeviceTools().filter((t) => t.pluginId === pluginId).map((t) => t.name);
2520
2799
  if (names.length === 0) {
2521
2800
  console.warn(
@@ -2541,7 +2820,7 @@ var init_bundleManager = __esm({
2541
2820
  const it = queue.shift();
2542
2821
  if (!it) return;
2543
2822
  try {
2544
- await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash);
2823
+ await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash, it.grants);
2545
2824
  okCount++;
2546
2825
  } catch (err) {
2547
2826
  failCount++;
@@ -2569,11 +2848,12 @@ var init_bundleManager = __esm({
2569
2848
  */
2570
2849
  async clearPlugin(pluginId) {
2571
2850
  const storage = getBundleStorage();
2572
- const index = await storage.loadIndex();
2573
- const entry = index[pluginId];
2851
+ const index = await loadCacheIndex(storage);
2852
+ const bundleUrl = pluginBundleUrl(pluginId);
2853
+ const entry = index[bundleUrl];
2574
2854
  if (entry) {
2575
- this.modules.delete(entry.bundleUrl);
2576
- delete index[pluginId];
2855
+ this.modules.delete(bundleUrl);
2856
+ delete index[bundleUrl];
2577
2857
  await storage.saveIndex(index);
2578
2858
  try {
2579
2859
  await storage.deleteBundle(entry.localPath);
@@ -2598,12 +2878,11 @@ var init_bundleManager = __esm({
2598
2878
  throw new Error("[BundleManager] tenant changed before load started \u2014 discarding (fail closed)");
2599
2879
  }
2600
2880
  if (this.modules.has(bundleUrl)) {
2601
- const prev = this.provenance.get(bundleUrl);
2602
- if (prev) this.provenance.set(bundleUrl, { ...prev, source: "memory", expected: expectedHash?.toLowerCase() ?? prev.expected });
2881
+ this.markMemoryHit(bundleUrl, expectedHash);
2603
2882
  return;
2604
2883
  }
2605
2884
  const storage = getBundleStorage();
2606
- const index = await storage.loadIndex();
2885
+ const index = await loadCacheIndex(storage);
2607
2886
  const expectedLc = expectedHash ? expectedHash.toLowerCase() : null;
2608
2887
  let content = null;
2609
2888
  let verifiedHash = null;
@@ -2666,30 +2945,90 @@ var init_bundleManager = __esm({
2666
2945
  console.log(
2667
2946
  `[BundleManager] RUNNING ${bundleUrl} v${version} sha=${(verifiedHash || "none").slice(0, 12)} (${servedFrom}${expectedLc && verifiedHash && expectedLc !== verifiedHash ? `, EXPECTED ${expectedLc.slice(0, 12)} \u2014 STALE` : ""})`
2668
2947
  );
2669
- this.registerToolsFromModule(mod, bundleUrl);
2670
2948
  }
2671
2949
  /**
2672
- * Pick up every `tools` section in an evaluated bundle and put it in the
2673
- * device tool registry. Runs for UI, hybrid and tools-only bundles alike: a
2674
- * plugin that ships both gets its tools registered without its UI mounting.
2950
+ * Register THIS plugin's device tools under the TRUSTED declaration, then
2951
+ * record that declaration on the disk-cache entry. Runs on every trusted load
2952
+ * — memory hit included — so the grants in force are always the latest the
2953
+ * manifest gave (a revoked grant stops applying on the next load, not the next
2954
+ * app start). Callers run it only after their stale check.
2675
2955
  */
2676
- registerToolsFromModule(mod, bundleUrl) {
2956
+ async commitTools(pluginId, bundleUrl, grants, tenant, epoch) {
2957
+ const declaration = {
2958
+ pluginId,
2959
+ capabilities: grants.capabilities,
2960
+ stateDomains: grants.stateDomains
2961
+ };
2962
+ this.registerToolsFromModule(this.modules.get(bundleUrl), declaration);
2963
+ await this.recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch);
2964
+ }
2965
+ /**
2966
+ * Put the plugin's own `tools` section in the device tool registry — under the
2967
+ * pluginId and grants the MANIFEST resolved, never the bundle's own `id`,
2968
+ * `capabilities` or `stateDomains`. A bundle is code the manifest authorised
2969
+ * by hash; what it may touch is the manifest's to say, same as its rendered
2970
+ * surface (resolvePluginRuntimeProps). Its own `id` would also let it register
2971
+ * as another plugin — including the host's platform tools, which a same-id
2972
+ * re-registration replaces.
2973
+ *
2974
+ * Only this plugin's export (the same lookup the component uses) is
2975
+ * registered. Another export's tools in the same bundle belong to another
2976
+ * plugin and register when THAT plugin's manifest loads it.
2977
+ */
2978
+ registerToolsFromModule(mod, { pluginId, capabilities, stateDomains }) {
2677
2979
  if (!mod) return;
2678
- for (const key of Object.keys(mod)) {
2679
- const val = mod[key];
2680
- if (!val || typeof val !== "object" || !Array.isArray(val.tools) || val.tools.length === 0) {
2681
- continue;
2682
- }
2683
- const pluginId = typeof val.id === "string" ? val.id : `${bundleUrl}#${key}`;
2684
- try {
2685
- registerDeviceTools(pluginId, val.tools, val.capabilities, val.stateDomains);
2686
- this.registeredToolPluginIds.add(pluginId);
2687
- } catch (err) {
2688
- console.warn(
2689
- `[BundleManager] tool registration failed for ${pluginId}:`,
2690
- err instanceof Error ? err.message : String(err)
2691
- );
2980
+ const own = this.findPluginExport(mod, pluginId, hasTools);
2981
+ const others = Object.keys(mod).filter((k) => hasTools(mod[k]) && mod[k] !== own);
2982
+ if (others.length > 0) {
2983
+ console.warn(
2984
+ `[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`
2985
+ );
2986
+ }
2987
+ if (!own) return;
2988
+ const ungranted = [
2989
+ ...(Array.isArray(own.stateDomains) ? own.stateDomains : []).filter((d) => !stateDomains?.includes(d)),
2990
+ ...Object.keys(own.capabilities || {}).filter((k) => own.capabilities[k] === true && !capabilities?.[k])
2991
+ ];
2992
+ if (ungranted.length > 0) {
2993
+ console.warn(
2994
+ `[BundleManager] ${pluginId}: bundle declares ${ungranted.join(", ")} but the manifest does not grant it \u2014 denied`
2995
+ );
2996
+ }
2997
+ try {
2998
+ registerDeviceTools(pluginId, own.tools, capabilities, stateDomains);
2999
+ this.registeredToolPluginIds.add(pluginId);
3000
+ } catch (err) {
3001
+ console.warn(
3002
+ `[BundleManager] tool registration failed for ${pluginId}:`,
3003
+ err instanceof Error ? err.message : String(err)
3004
+ );
3005
+ }
3006
+ }
3007
+ /**
3008
+ * Record the trusted declaration on this tenant's disk-cache entry for the
3009
+ * bytes RUNNING — the recovery path (tools/loader loadFromDiskCache) has no
3010
+ * manifest and re-registers with exactly this. Only onto an entry this tenant
3011
+ * owns whose hash is the running hash: the grant was issued for these bytes,
3012
+ * and those are the bytes recovery will evaluate. Best-effort — a lost write
3013
+ * leaves no `trusted`, which recovery treats as fail closed.
3014
+ */
3015
+ async recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch) {
3016
+ const key = JSON.stringify(declaration);
3017
+ if (this.recordedDeclarations.get(bundleUrl) === key) return;
3018
+ try {
3019
+ const storage = getBundleStorage();
3020
+ const index = await loadCacheIndex(storage);
3021
+ const entry = index[bundleUrl];
3022
+ const running = this.provenance.get(bundleUrl)?.hash;
3023
+ if (!entry || entry.tenant !== tenant || !running || entry.bundleHash?.toLowerCase() !== running) return;
3024
+ if (this.isStale(epoch, tenant)) return;
3025
+ if (JSON.stringify(entry.trusted) !== key) {
3026
+ index[bundleUrl] = { ...entry, trusted: declaration };
3027
+ await storage.saveIndex(index);
2692
3028
  }
3029
+ this.recordedDeclarations.set(bundleUrl, key);
3030
+ } catch (err) {
3031
+ console.warn("[BundleManager] could not record trusted declaration:", err);
2693
3032
  }
2694
3033
  }
2695
3034
  /**
@@ -2772,29 +3111,26 @@ var init_bundleManager = __esm({
2772
3111
  return materialized;
2773
3112
  }
2774
3113
  // ── Private: component extraction ───────────────────────────
3114
+ /** Find a plugin component in a loaded module. */
3115
+ getComponentFromMemory(pluginId, bundleUrl) {
3116
+ return this.findPluginExport(this.modules.get(bundleUrl), pluginId, hasComponent);
3117
+ }
2775
3118
  /**
2776
- * Find a plugin component in a loaded module.
2777
- * Handles: default export, direct key match, multi-plugin .id match.
3119
+ * Find THIS plugin's export in a loaded module — the one lookup both the
3120
+ * component and the device tools use, so a plugin's UI and its tools always
3121
+ * come from the same export. `accept` says what the caller needs from it.
3122
+ * Handles: direct key match, PascalCase key, default export, multi-plugin .id match.
2778
3123
  */
2779
- getComponentFromMemory(pluginId, bundleUrl) {
2780
- const mod = this.modules.get(bundleUrl);
3124
+ findPluginExport(mod, pluginId, accept) {
2781
3125
  if (!mod) return null;
2782
3126
  const shortName = pluginId.includes(":") ? pluginId.split(":").pop() : pluginId;
2783
- if (mod[pluginId] && typeof mod[pluginId].Component === "function") {
2784
- return mod[pluginId];
2785
- }
3127
+ if (accept(mod[pluginId])) return mod[pluginId];
2786
3128
  const pascal = shortName.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
2787
- if (mod[pascal] && typeof mod[pascal] === "object" && typeof mod[pascal].Component === "function") {
2788
- return mod[pascal];
2789
- }
2790
- if (mod.default && typeof mod.default === "object" && typeof mod.default.Component === "function") {
2791
- return mod.default;
2792
- }
3129
+ if (accept(mod[pascal])) return mod[pascal];
3130
+ if (accept(mod.default)) return mod.default;
2793
3131
  for (const key of Object.keys(mod)) {
2794
3132
  const val = mod[key];
2795
- if (val && typeof val === "object" && val.id === shortName && typeof val.Component === "function") {
2796
- return val;
2797
- }
3133
+ if (val && (val.id === pluginId || val.id === shortName) && accept(val)) return val;
2798
3134
  }
2799
3135
  return null;
2800
3136
  }
@@ -2853,7 +3189,8 @@ async function ensureDeviceToolsLoaded({ force = false } = {}) {
2853
3189
  m.id,
2854
3190
  bundleUrl,
2855
3191
  m.version || "1.0.0",
2856
- getNativeBundleHash(m) || void 0
3192
+ getNativeBundleHash(m) || void 0,
3193
+ resolvePluginGrants({ pluginId: m.id, manifest: m })
2857
3194
  );
2858
3195
  } catch (err) {
2859
3196
  console.warn(`[deviceTools] ${m.id} failed to load:`, err?.message || String(err));
@@ -2872,7 +3209,7 @@ async function ensureDeviceToolsLoaded({ force = false } = {}) {
2872
3209
  }
2873
3210
  async function loadFromDiskCache() {
2874
3211
  try {
2875
- const index = await getBundleStorage().loadIndex();
3212
+ const index = await loadCacheIndex(getBundleStorage());
2876
3213
  const entries = Object.values(index || {});
2877
3214
  if (entries.length === 0) return [];
2878
3215
  const tenant = (0, import_core10.getTenant)();
@@ -2885,10 +3222,16 @@ async function loadFromDiskCache() {
2885
3222
  if (skipped > 0) {
2886
3223
  console.log(`[deviceTools] disk-cache recovery: ${mine.length} for "${tenant}", skipped ${skipped} not owned by it`);
2887
3224
  }
2888
- if (mine.length === 0) return [];
2889
- await Promise.all(mine.map(async (e) => {
3225
+ const declared = mine.filter((e) => !!e.trusted?.pluginId);
3226
+ if (declared.length < mine.length) {
3227
+ console.log(
3228
+ `[deviceTools] disk-cache recovery: skipped ${mine.length - declared.length} with no recorded manifest grant (fail closed)`
3229
+ );
3230
+ }
3231
+ if (declared.length === 0) return [];
3232
+ await Promise.all(declared.map(async (e) => {
2890
3233
  try {
2891
- await bundleManager.loadTools("cache:" + e.bundleUrl, e.bundleUrl, e.version, e.bundleHash);
3234
+ await bundleManager.loadTools(e.trusted.pluginId, e.bundleUrl, e.version, e.bundleHash, e.trusted);
2892
3235
  } catch {
2893
3236
  }
2894
3237
  }));
@@ -2909,6 +3252,7 @@ var init_loader = __esm({
2909
3252
  init_bundleManager();
2910
3253
  init_registry();
2911
3254
  init_storage();
3255
+ init_pluginRuntime();
2912
3256
  import_core10 = require("@ateam-ai-mobile/core");
2913
3257
  inflight = null;
2914
3258
  loadedOnce = false;
@@ -2983,6 +3327,7 @@ __export(index_exports, {
2983
3327
  listPlugins: () => listPlugins,
2984
3328
  nativePluginRegistry: () => nativePluginRegistry,
2985
3329
  neverObserved: () => neverObserved,
3330
+ pluginBundleUrl: () => pluginBundleUrl,
2986
3331
  register: () => register,
2987
3332
  registerBundleModules: () => registerBundleModules,
2988
3333
  registerDeviceTools: () => registerDeviceTools,
@@ -2991,6 +3336,7 @@ __export(index_exports, {
2991
3336
  registerService: () => registerService,
2992
3337
  registerTools: () => registerTools,
2993
3338
  resetDeviceToolLoader: () => resetDeviceToolLoader,
3339
+ resolvePluginGrants: () => resolvePluginGrants,
2994
3340
  resolvePluginRuntimeProps: () => resolvePluginRuntimeProps,
2995
3341
  resolvePluginUrl: () => resolvePluginUrl,
2996
3342
  setBundleStorage: () => setBundleStorage,
@@ -3016,166 +3362,12 @@ __export(index_exports, {
3016
3362
  });
3017
3363
  module.exports = __toCommonJS(index_exports);
3018
3364
  init_sdk();
3019
-
3020
- // src/sdk/device-state.ts
3021
- var device_state_exports = {};
3022
- __export(device_state_exports, {
3023
- DeviceBridgeError: () => DeviceBridgeError,
3024
- explainCapability: () => explainCapability,
3025
- isActionable: () => isActionable,
3026
- neverObserved: () => neverObserved
3027
- });
3028
- var DeviceBridgeError = class extends Error {
3029
- constructor(code, message, opts) {
3030
- super(message);
3031
- this.name = "DeviceBridgeError";
3032
- this.code = code;
3033
- this.domain = opts?.domain;
3034
- this.recoverable = opts?.recoverable;
3035
- }
3036
- };
3037
- var CONFIDENCE_ORDER = {
3038
- unknown: 0,
3039
- low: 1,
3040
- medium: 2,
3041
- high: 3
3042
- };
3043
- function isActionable(quality, budget) {
3044
- const { freshness, confidence } = quality;
3045
- if (freshness.state === "unknown" || freshness.state === "stale") return false;
3046
- if (freshness.ageMs === null || freshness.ageMs > budget.maxAgeMs) return false;
3047
- if (budget.minConfidence) {
3048
- const have = CONFIDENCE_ORDER[confidence?.level ?? "unknown"];
3049
- if (have < CONFIDENCE_ORDER[budget.minConfidence]) return false;
3050
- }
3051
- return true;
3052
- }
3053
- function neverObserved() {
3054
- return { state: "unknown", observedAt: null, ageMs: null };
3055
- }
3056
- function explainCapability(status) {
3057
- if (status.support === "unsupported") return "this device does not have it";
3058
- if (status.permission === "denied") return "permission was denied";
3059
- if (status.permission === "restricted") return "permission is restricted";
3060
- if (status.permission === "notDetermined") return "permission has not been asked for yet";
3061
- if (status.permission === "limited") return "only limited access was granted";
3062
- if (status.restriction === "policyRestricted") return "this plugin did not declare it";
3063
- if (status.restriction === "backgroundRestricted") return "the app is in the background";
3064
- if (status.restriction === "lowPowerRestricted") return "the device is in low-power mode";
3065
- if (status.availability === "busy") return "it is in use by something else";
3066
- if (status.availability === "temporarilyUnavailable") return "it is temporarily unavailable";
3067
- if (status.availability === "unavailable") return "it is unavailable";
3068
- if (status.support === "unknown" || status.availability === "unknown") {
3069
- return "its state could not be determined";
3070
- }
3071
- return null;
3072
- }
3073
-
3074
- // src/index.ts
3365
+ init_device_state();
3366
+ init_device_state();
3075
3367
  init_deviceStateHost();
3076
-
3077
- // src/voiceHost.ts
3078
- var _provider2 = null;
3079
- function setVoiceProvider(provider) {
3080
- _provider2 = provider || null;
3081
- }
3082
- function implementedVoice() {
3083
- return !!_provider2;
3084
- }
3085
- function voiceDenial(why) {
3086
- const err = new Error(`voice: ${why}`);
3087
- err.code = "RESTRICTED";
3088
- return err;
3089
- }
3090
- function deniedVoice(why) {
3091
- const fail = () => {
3092
- throw voiceDenial(why);
3093
- };
3094
- return {
3095
- async start() {
3096
- return fail();
3097
- },
3098
- stop() {
3099
- fail();
3100
- },
3101
- setMuted() {
3102
- fail();
3103
- },
3104
- prompt() {
3105
- fail();
3106
- },
3107
- observe() {
3108
- fail();
3109
- },
3110
- getSnapshot() {
3111
- return {
3112
- state: "error",
3113
- sessionId: null,
3114
- muted: false,
3115
- speaking: false,
3116
- listening: false,
3117
- userSpeaking: false,
3118
- lastUserSpeechEndedAt: null,
3119
- lastError: why
3120
- };
3121
- },
3122
- subscribe() {
3123
- return fail();
3124
- },
3125
- transcript() {
3126
- return [];
3127
- }
3128
- };
3129
- }
3130
- function scopedVoice(provider, pluginId) {
3131
- return {
3132
- start: (opts) => provider.start(opts, pluginId),
3133
- stop: () => provider.stop(pluginId),
3134
- setMuted: (muted) => provider.setMuted(muted, pluginId),
3135
- prompt: (input) => provider.prompt(input, pluginId),
3136
- observe: (text) => provider.observe(text, pluginId),
3137
- getSnapshot: () => provider.getSnapshot(pluginId),
3138
- subscribe: (cb) => provider.subscribe(cb, pluginId),
3139
- transcript: () => provider.transcript(pluginId)
3140
- };
3141
- }
3142
- function gateVoice(declared, pluginId) {
3143
- if (!declared) {
3144
- return deniedVoice(
3145
- "not declared in this plugin's capabilities. Add `capabilities: { voice: true }` to use it."
3146
- );
3147
- }
3148
- if (!pluginId) {
3149
- return deniedVoice(
3150
- "no plugin identity \u2014 voice fails closed without a caller to scope the session to."
3151
- );
3152
- }
3153
- return _provider2 ? scopedVoice(_provider2, pluginId) : deniedVoice(
3154
- "no voice session in this build \u2014 the app has not mounted a VoiceSessionHost / called setVoiceProvider."
3155
- );
3156
- }
3157
-
3158
- // src/index.ts
3368
+ init_voiceHost();
3159
3369
  init_host();
3160
-
3161
- // src/pluginRuntime.ts
3162
- init_host();
3163
- init_deviceStateHost();
3164
- function mergeCaps(decl) {
3165
- if (!decl) return void 0;
3166
- return { ...decl.native, ...decl.capabilities };
3167
- }
3168
- function resolvePluginRuntimeProps(src) {
3169
- const trusted = src.manifest ?? src.plugin ?? null;
3170
- const capabilities = mergeCaps(trusted);
3171
- const stateDomains = trusted?.stateDomains;
3172
- const native = gatePluginNative(getPluginNative(), capabilities, src.pluginId);
3173
- const deviceState = gateDeviceState(stateDomains);
3174
- const voice = gateVoice(!!capabilities?.voice, src.pluginId);
3175
- return { device: native, native, deviceState, voice };
3176
- }
3177
-
3178
- // src/index.ts
3370
+ init_pluginRuntime();
3179
3371
  init_registry();
3180
3372
  init_loader();
3181
3373
 
@@ -3293,6 +3485,8 @@ init_native();
3293
3485
  var import_react11 = require("react");
3294
3486
  init_bundleManager();
3295
3487
  init_host();
3488
+ init_pluginRuntime();
3489
+ init_bundleUrl();
3296
3490
  function extractBundleUrl(pluginId, manifest) {
3297
3491
  const render = manifest.render;
3298
3492
  if (!render || typeof render !== "object") return { url: null, source: "none" };
@@ -3300,7 +3494,7 @@ function extractBundleUrl(pluginId, manifest) {
3300
3494
  const hasNativeComponent = !!(render.reactNative?.component || render.component);
3301
3495
  if (hasNativeComponent) {
3302
3496
  return {
3303
- url: `/api/ui-plugins/${encodeURIComponent(pluginId)}/bundle.js`,
3497
+ url: pluginBundleUrl(pluginId),
3304
3498
  source: injected ? "ignored-explicit" : "derived-default",
3305
3499
  ...injected && { ignored: injected }
3306
3500
  };
@@ -3330,6 +3524,8 @@ function usePlugin(pluginId, manifest) {
3330
3524
  const bundleHash = extractBundleHash(manifest);
3331
3525
  const componentId = extractComponentId(manifest);
3332
3526
  const lookupId = componentId || pluginId;
3527
+ const grants = resolvePluginGrants({ pluginId, manifest });
3528
+ const grantsKey = JSON.stringify(grants);
3333
3529
  if (ignoredUrl) {
3334
3530
  console.warn(`[usePlugin] ${pluginId}: IGNORED connector-injected bundleUrl="${ignoredUrl}". The host derives bundle URLs from pluginId via the canonical route.`);
3335
3531
  }
@@ -3352,7 +3548,7 @@ function usePlugin(pluginId, manifest) {
3352
3548
  }
3353
3549
  if (bundleUrl) {
3354
3550
  try {
3355
- const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash);
3551
+ const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash, grants);
3356
3552
  if (cancelled) return;
3357
3553
  setPlugin(component);
3358
3554
  setLoading(false);
@@ -3389,7 +3585,7 @@ function usePlugin(pluginId, manifest) {
3389
3585
  return () => {
3390
3586
  cancelled = true;
3391
3587
  };
3392
- }, [pluginId, lookupId, bundleUrl, bundleHash, version]);
3588
+ }, [pluginId, lookupId, bundleUrl, bundleHash, version, grantsKey]);
3393
3589
  return { plugin, loading, error, bundleMissing };
3394
3590
  }
3395
3591
 
@@ -3465,6 +3661,7 @@ function useRemotePlugins(solutionId) {
3465
3661
 
3466
3662
  // src/index.ts
3467
3663
  init_plugins();
3664
+ init_bundleUrl();
3468
3665
  init_SurfaceDrawer();
3469
3666
  init_SurfaceCard();
3470
3667
  init_SurfaceFullscreen();
@@ -3538,6 +3735,7 @@ init_SurfaceDispatcher();
3538
3735
  listPlugins,
3539
3736
  nativePluginRegistry,
3540
3737
  neverObserved,
3738
+ pluginBundleUrl,
3541
3739
  register,
3542
3740
  registerBundleModules,
3543
3741
  registerDeviceTools,
@@ -3546,6 +3744,7 @@ init_SurfaceDispatcher();
3546
3744
  registerService,
3547
3745
  registerTools,
3548
3746
  resetDeviceToolLoader,
3747
+ resolvePluginGrants,
3549
3748
  resolvePluginRuntimeProps,
3550
3749
  resolvePluginUrl,
3551
3750
  setBundleStorage,