@ateam-ai-mobile/plugin-runtime 0.1.7 → 0.1.8

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
@@ -395,7 +395,8 @@ function gatePluginNative(native, declared, pluginId) {
395
395
  // every method refuses by name. Both are better than the `undefined` this
396
396
  // used to be, which crashed with "cannot read properties of undefined".
397
397
  bluetooth: caps.bluetooth ? native.bluetooth : blocked("bluetooth"),
398
- vision: caps.vision ? native.vision : blocked("vision")
398
+ vision: caps.vision ? native.vision : blocked("vision"),
399
+ maps: caps.maps ? native.maps : blocked("maps")
399
400
  // What this plugin DECLARED, carried on the object it already receives.
400
401
  //
401
402
  // It is how a COMPONENT can be capability-gated: <CameraPreview> is handed
@@ -455,7 +456,8 @@ var init_host = __esm({
455
456
  scanner: unavailable("scanner"),
456
457
  nfc: unavailable("nfc"),
457
458
  bluetooth: unavailable("bluetooth"),
458
- vision: unavailable("vision")
459
+ vision: unavailable("vision"),
460
+ maps: unavailable("maps")
459
461
  };
460
462
  _pluginNative = noopNative;
461
463
  _cameraPreview = null;
@@ -814,6 +816,19 @@ function gateDeviceState(declared) {
814
816
  }
815
817
  return out;
816
818
  }
819
+ function withoutSubscribe(state) {
820
+ if (!state) return state;
821
+ const out = {};
822
+ for (const domain of ALL_DOMAINS) {
823
+ const d = state[domain];
824
+ out[domain] = d && Object.assign(Object.create(d), {
825
+ subscribe() {
826
+ 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.");
827
+ }
828
+ });
829
+ }
830
+ return out;
831
+ }
817
832
  function implementedStateDomains() {
818
833
  return ALL_DOMAINS.filter((d) => !!_provider[d]);
819
834
  }
@@ -839,6 +854,121 @@ var init_deviceStateHost = __esm({
839
854
  }
840
855
  });
841
856
 
857
+ // src/voiceHost.ts
858
+ function setVoiceProvider(provider) {
859
+ _provider2 = provider || null;
860
+ }
861
+ function implementedVoice() {
862
+ return !!_provider2;
863
+ }
864
+ function voiceDenial(why) {
865
+ const err = new Error(`voice: ${why}`);
866
+ err.code = "RESTRICTED";
867
+ return err;
868
+ }
869
+ function deniedVoice(why) {
870
+ const fail = () => {
871
+ throw voiceDenial(why);
872
+ };
873
+ return {
874
+ async start() {
875
+ return fail();
876
+ },
877
+ stop() {
878
+ fail();
879
+ },
880
+ setMuted() {
881
+ fail();
882
+ },
883
+ prompt() {
884
+ fail();
885
+ },
886
+ observe() {
887
+ fail();
888
+ },
889
+ getSnapshot() {
890
+ return {
891
+ state: "error",
892
+ sessionId: null,
893
+ muted: false,
894
+ speaking: false,
895
+ listening: false,
896
+ userSpeaking: false,
897
+ lastUserSpeechEndedAt: null,
898
+ lastDeviceTool: null,
899
+ authorizedSessionId: null,
900
+ lastError: why
901
+ };
902
+ },
903
+ subscribe() {
904
+ return fail();
905
+ },
906
+ transcript() {
907
+ return [];
908
+ }
909
+ };
910
+ }
911
+ function scopedVoice(provider, pluginId) {
912
+ return {
913
+ start: (opts) => provider.start(opts, pluginId),
914
+ stop: () => provider.stop(pluginId),
915
+ setMuted: (muted) => provider.setMuted(muted, pluginId),
916
+ prompt: (input) => provider.prompt(input, pluginId),
917
+ observe: (text) => provider.observe(text, pluginId),
918
+ getSnapshot: () => provider.getSnapshot(pluginId),
919
+ subscribe: (cb) => provider.subscribe(cb, pluginId),
920
+ transcript: () => provider.transcript(pluginId)
921
+ };
922
+ }
923
+ function gateVoice(declared, pluginId) {
924
+ if (!declared) {
925
+ return deniedVoice(
926
+ "not declared in this plugin's capabilities. Add `capabilities: { voice: true }` to use it."
927
+ );
928
+ }
929
+ if (!pluginId) {
930
+ return deniedVoice(
931
+ "no plugin identity \u2014 voice fails closed without a caller to scope the session to."
932
+ );
933
+ }
934
+ return _provider2 ? scopedVoice(_provider2, pluginId) : deniedVoice(
935
+ "no voice session in this build \u2014 the app has not mounted a VoiceSessionHost / called setVoiceProvider."
936
+ );
937
+ }
938
+ var _provider2;
939
+ var init_voiceHost = __esm({
940
+ "src/voiceHost.ts"() {
941
+ "use strict";
942
+ _provider2 = null;
943
+ }
944
+ });
945
+
946
+ // src/pluginRuntime.ts
947
+ function mergeCaps(decl) {
948
+ if (!decl) return void 0;
949
+ return { ...decl.native, ...decl.capabilities };
950
+ }
951
+ function resolvePluginGrants(src) {
952
+ const trusted = src.manifest ?? src.plugin ?? null;
953
+ const stateDomains = trusted?.stateDomains ? [...trusted.stateDomains] : void 0;
954
+ return { capabilities: mergeCaps(trusted), stateDomains };
955
+ }
956
+ function resolvePluginRuntimeProps(src) {
957
+ const { capabilities, stateDomains } = resolvePluginGrants(src);
958
+ const native = gatePluginNative(getPluginNative(), capabilities, src.pluginId);
959
+ const deviceState = gateDeviceState(stateDomains);
960
+ const voice = gateVoice(!!capabilities?.voice, src.pluginId);
961
+ return { device: native, native, deviceState, voice };
962
+ }
963
+ var init_pluginRuntime = __esm({
964
+ "src/pluginRuntime.ts"() {
965
+ "use strict";
966
+ init_host();
967
+ init_deviceStateHost();
968
+ init_voiceHost();
969
+ }
970
+ });
971
+
842
972
  // src/bridge/native.ts
843
973
  function buildContext(pluginId, connectorId) {
844
974
  const user = (0, import_core2.getUser)();
@@ -1086,8 +1216,9 @@ function unregisterDeviceTools(pluginId) {
1086
1216
  if (entry.pluginId === pluginId) registry.delete(name);
1087
1217
  }
1088
1218
  }
1089
- function hasDeviceTool(name) {
1090
- return registry.has(name);
1219
+ function hasDeviceTool(name, pluginId) {
1220
+ const entry = registry.get(name);
1221
+ return !!entry && (pluginId === void 0 || entry.pluginId === pluginId);
1091
1222
  }
1092
1223
  function listDeviceTools() {
1093
1224
  return Array.from(registry.entries()).map(([name, { pluginId, tool }]) => ({
@@ -1106,8 +1237,11 @@ async function callDeviceTool(name, args = {}, opts = {}) {
1106
1237
  let timer;
1107
1238
  try {
1108
1239
  const build = contextProvider || defaultContext;
1240
+ const built = build(entry.pluginId, entry.capabilities, entry.stateDomains);
1109
1241
  const ctx = {
1110
- ...build(entry.pluginId, entry.capabilities, entry.stateDomains),
1242
+ ...built,
1243
+ // Whoever built the context, a call cannot own a subscription.
1244
+ deviceState: withoutSubscribe(built.deviceState),
1111
1245
  signal: opts.signal
1112
1246
  };
1113
1247
  const result = await Promise.race([
@@ -2286,7 +2420,7 @@ var init_storage = __esm({
2286
2420
  });
2287
2421
 
2288
2422
  // src/bundles/bundleManager.ts
2289
- var import_react9, import_react10, ReactNative, import_core9, ReactShim, PluginSDKShim, requireShim, CACHE_TTL, BundleManager, bundleManager;
2423
+ var import_react9, import_react10, ReactNative, import_core9, ReactShim, PluginSDKShim, requireShim, CACHE_TTL, hasComponent, hasTools, BundleManager, bundleManager;
2290
2424
  var init_bundleManager = __esm({
2291
2425
  "src/bundles/bundleManager.ts"() {
2292
2426
  "use strict";
@@ -2357,6 +2491,8 @@ var init_bundleManager = __esm({
2357
2491
  return {};
2358
2492
  };
2359
2493
  CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
2494
+ hasComponent = (val) => !!val && typeof val === "object" && typeof val.Component === "function";
2495
+ hasTools = (val) => !!val && typeof val === "object" && Array.isArray(val.tools) && val.tools.length > 0;
2360
2496
  BundleManager = class {
2361
2497
  constructor() {
2362
2498
  /** In-memory module cache: bundleUrl → evaluated module.exports. Holds ONE
@@ -2387,6 +2523,9 @@ var init_bundleManager = __esm({
2387
2523
  * of the previous tenant's tools.
2388
2524
  */
2389
2525
  this.registeredToolPluginIds = /* @__PURE__ */ new Set();
2526
+ /** bundleUrl → the trusted declaration last recorded on its disk-cache entry
2527
+ * this session, so a memory hit with unchanged grants skips the index I/O. */
2528
+ this.recordedDeclarations = /* @__PURE__ */ new Map();
2390
2529
  /**
2391
2530
  * Enforce the in-memory tenant boundary. Call at the top of every public load
2392
2531
  * path, BEFORE any cache read.
@@ -2432,6 +2571,7 @@ var init_bundleManager = __esm({
2432
2571
  }
2433
2572
  }
2434
2573
  this.registeredToolPluginIds.clear();
2574
+ this.recordedDeclarations.clear();
2435
2575
  this.modules.clear();
2436
2576
  this.provenance.clear();
2437
2577
  this.inflight.clear();
@@ -2443,13 +2583,15 @@ var init_bundleManager = __esm({
2443
2583
  isStale(epoch, tenant) {
2444
2584
  return this.epoch !== epoch || (0, import_core9.getTenant)() !== tenant;
2445
2585
  }
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) {
2586
+ /** Record a memory serve: the running bytes are the already-evaluated module,
2587
+ * and `expected` is refreshed from the manifest hash the caller JUST passed —
2588
+ * so after a redeploy the record reads hash !== expected (STALE) instead of
2589
+ * the old in-sync pair. The ONE place both memory paths (load()'s early return
2590
+ * and loadModule's) record reuse. No-op if nothing was recorded yet. */
2591
+ markMemoryHit(bundleUrl, expectedHash) {
2450
2592
  const prev = this.provenance.get(bundleUrl);
2451
- if (prev && prev.source !== "memory") {
2452
- this.provenance.set(bundleUrl, { ...prev, source: "memory" });
2593
+ if (prev) {
2594
+ this.provenance.set(bundleUrl, { ...prev, source: "memory", expected: expectedHash?.toLowerCase() ?? prev.expected });
2453
2595
  }
2454
2596
  }
2455
2597
  // ── Public API ──────────────────────────────────────────────
@@ -2461,13 +2603,21 @@ var init_bundleManager = __esm({
2461
2603
  * provided for any bundle the user has not previously verified — without
2462
2604
  * it, a tampered bundle is `eval`'d as JS with full RN runtime + every
2463
2605
  * native capability the manifest declares.
2606
+ *
2607
+ * `grants` is what the MANIFEST grants this plugin (resolvePluginGrants) — the
2608
+ * only authority its device tools are registered under. Omitted (e.g. a
2609
+ * prewarm from the slim plugin list) → the bundle is loaded but its tools are
2610
+ * NOT registered: without the manifest there is nothing trusted to gate them
2611
+ * on, and registering with nothing would downgrade a registration the
2612
+ * manifest-driven tool loader already made.
2464
2613
  */
2465
- async load(pluginId, bundleUrl, version, expectedHash) {
2614
+ async load(pluginId, bundleUrl, version, expectedHash, grants) {
2466
2615
  const tenant = this.requireTenant();
2467
2616
  const epoch = this.epoch;
2468
2617
  const cached = this.getComponentFromMemory(pluginId, bundleUrl);
2469
2618
  if (cached) {
2470
- this.markMemoryHit(bundleUrl);
2619
+ this.markMemoryHit(bundleUrl, expectedHash);
2620
+ if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2471
2621
  return cached;
2472
2622
  }
2473
2623
  let modulePromise = this.inflight.get(bundleUrl);
@@ -2484,6 +2634,7 @@ var init_bundleManager = __esm({
2484
2634
  if (this.isStale(epoch, tenant)) {
2485
2635
  throw new Error("[BundleManager] tenant changed while loading \u2014 discarding (fail closed)");
2486
2636
  }
2637
+ if (grants) await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2487
2638
  const component = this.getComponentFromMemory(pluginId, bundleUrl);
2488
2639
  if (!component) {
2489
2640
  throw new Error(
@@ -2498,8 +2649,14 @@ var init_bundleManager = __esm({
2498
2649
  * Same download, same integrity check, same evaluation as `load()`; the only
2499
2650
  * difference is that a bundle exporting tools and no `Component` is a success
2500
2651
  * here rather than "no component found". Returns the tool names registered.
2652
+ *
2653
+ * `grants` is REQUIRED: the tools are registered under `pluginId` with exactly
2654
+ * what the manifest grants (see load()), never what the bundle claims.
2501
2655
  */
2502
- async loadTools(pluginId, bundleUrl, version, expectedHash) {
2656
+ async loadTools(pluginId, bundleUrl, version, expectedHash, grants) {
2657
+ if (!grants) {
2658
+ throw new Error(`[BundleManager] loadTools(${pluginId}) needs the manifest's grants (resolvePluginGrants) \u2014 refusing to register tools on the bundle's own claim`);
2659
+ }
2503
2660
  const tenant = this.requireTenant();
2504
2661
  const epoch = this.epoch;
2505
2662
  let modulePromise = this.inflight.get(bundleUrl);
@@ -2516,6 +2673,7 @@ var init_bundleManager = __esm({
2516
2673
  if (this.isStale(epoch, tenant)) {
2517
2674
  throw new Error("[BundleManager] tenant changed while loading tools \u2014 discarding (fail closed)");
2518
2675
  }
2676
+ await this.commitTools(pluginId, bundleUrl, grants, tenant, epoch);
2519
2677
  const names = listDeviceTools().filter((t) => t.pluginId === pluginId).map((t) => t.name);
2520
2678
  if (names.length === 0) {
2521
2679
  console.warn(
@@ -2541,7 +2699,7 @@ var init_bundleManager = __esm({
2541
2699
  const it = queue.shift();
2542
2700
  if (!it) return;
2543
2701
  try {
2544
- await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash);
2702
+ await this.load(it.pluginId, it.bundleUrl, it.version, it.bundleHash, it.grants);
2545
2703
  okCount++;
2546
2704
  } catch (err) {
2547
2705
  failCount++;
@@ -2598,8 +2756,7 @@ var init_bundleManager = __esm({
2598
2756
  throw new Error("[BundleManager] tenant changed before load started \u2014 discarding (fail closed)");
2599
2757
  }
2600
2758
  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 });
2759
+ this.markMemoryHit(bundleUrl, expectedHash);
2603
2760
  return;
2604
2761
  }
2605
2762
  const storage = getBundleStorage();
@@ -2666,30 +2823,90 @@ var init_bundleManager = __esm({
2666
2823
  console.log(
2667
2824
  `[BundleManager] RUNNING ${bundleUrl} v${version} sha=${(verifiedHash || "none").slice(0, 12)} (${servedFrom}${expectedLc && verifiedHash && expectedLc !== verifiedHash ? `, EXPECTED ${expectedLc.slice(0, 12)} \u2014 STALE` : ""})`
2668
2825
  );
2669
- this.registerToolsFromModule(mod, bundleUrl);
2670
2826
  }
2671
2827
  /**
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.
2828
+ * Register THIS plugin's device tools under the TRUSTED declaration, then
2829
+ * record that declaration on the disk-cache entry. Runs on every trusted load
2830
+ * — memory hit included — so the grants in force are always the latest the
2831
+ * manifest gave (a revoked grant stops applying on the next load, not the next
2832
+ * app start). Callers run it only after their stale check.
2675
2833
  */
2676
- registerToolsFromModule(mod, bundleUrl) {
2834
+ async commitTools(pluginId, bundleUrl, grants, tenant, epoch) {
2835
+ const declaration = {
2836
+ pluginId,
2837
+ capabilities: grants.capabilities,
2838
+ stateDomains: grants.stateDomains
2839
+ };
2840
+ this.registerToolsFromModule(this.modules.get(bundleUrl), declaration);
2841
+ await this.recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch);
2842
+ }
2843
+ /**
2844
+ * Put the plugin's own `tools` section in the device tool registry — under the
2845
+ * pluginId and grants the MANIFEST resolved, never the bundle's own `id`,
2846
+ * `capabilities` or `stateDomains`. A bundle is code the manifest authorised
2847
+ * by hash; what it may touch is the manifest's to say, same as its rendered
2848
+ * surface (resolvePluginRuntimeProps). Its own `id` would also let it register
2849
+ * as another plugin — including the host's platform tools, which a same-id
2850
+ * re-registration replaces.
2851
+ *
2852
+ * Only this plugin's export (the same lookup the component uses) is
2853
+ * registered. Another export's tools in the same bundle belong to another
2854
+ * plugin and register when THAT plugin's manifest loads it.
2855
+ */
2856
+ registerToolsFromModule(mod, { pluginId, capabilities, stateDomains }) {
2677
2857
  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
- );
2858
+ const own = this.findPluginExport(mod, pluginId, hasTools);
2859
+ const others = Object.keys(mod).filter((k) => hasTools(mod[k]) && mod[k] !== own);
2860
+ if (others.length > 0) {
2861
+ console.warn(
2862
+ `[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`
2863
+ );
2864
+ }
2865
+ if (!own) return;
2866
+ const ungranted = [
2867
+ ...(Array.isArray(own.stateDomains) ? own.stateDomains : []).filter((d) => !stateDomains?.includes(d)),
2868
+ ...Object.keys(own.capabilities || {}).filter((k) => own.capabilities[k] === true && !capabilities?.[k])
2869
+ ];
2870
+ if (ungranted.length > 0) {
2871
+ console.warn(
2872
+ `[BundleManager] ${pluginId}: bundle declares ${ungranted.join(", ")} but the manifest does not grant it \u2014 denied`
2873
+ );
2874
+ }
2875
+ try {
2876
+ registerDeviceTools(pluginId, own.tools, capabilities, stateDomains);
2877
+ this.registeredToolPluginIds.add(pluginId);
2878
+ } catch (err) {
2879
+ console.warn(
2880
+ `[BundleManager] tool registration failed for ${pluginId}:`,
2881
+ err instanceof Error ? err.message : String(err)
2882
+ );
2883
+ }
2884
+ }
2885
+ /**
2886
+ * Record the trusted declaration on this tenant's disk-cache entry for the
2887
+ * bytes RUNNING — the recovery path (tools/loader loadFromDiskCache) has no
2888
+ * manifest and re-registers with exactly this. Only onto an entry this tenant
2889
+ * owns whose hash is the running hash: the grant was issued for these bytes,
2890
+ * and those are the bytes recovery will evaluate. Best-effort — a lost write
2891
+ * leaves no `trusted`, which recovery treats as fail closed.
2892
+ */
2893
+ async recordTrustedDeclaration(bundleUrl, declaration, tenant, epoch) {
2894
+ const key = JSON.stringify(declaration);
2895
+ if (this.recordedDeclarations.get(bundleUrl) === key) return;
2896
+ try {
2897
+ const storage = getBundleStorage();
2898
+ const index = await storage.loadIndex();
2899
+ const entry = index[bundleUrl];
2900
+ const running = this.provenance.get(bundleUrl)?.hash;
2901
+ if (!entry || entry.tenant !== tenant || !running || entry.bundleHash?.toLowerCase() !== running) return;
2902
+ if (this.isStale(epoch, tenant)) return;
2903
+ if (JSON.stringify(entry.trusted) !== key) {
2904
+ index[bundleUrl] = { ...entry, trusted: declaration };
2905
+ await storage.saveIndex(index);
2692
2906
  }
2907
+ this.recordedDeclarations.set(bundleUrl, key);
2908
+ } catch (err) {
2909
+ console.warn("[BundleManager] could not record trusted declaration:", err);
2693
2910
  }
2694
2911
  }
2695
2912
  /**
@@ -2772,29 +2989,26 @@ var init_bundleManager = __esm({
2772
2989
  return materialized;
2773
2990
  }
2774
2991
  // ── Private: component extraction ───────────────────────────
2992
+ /** Find a plugin component in a loaded module. */
2993
+ getComponentFromMemory(pluginId, bundleUrl) {
2994
+ return this.findPluginExport(this.modules.get(bundleUrl), pluginId, hasComponent);
2995
+ }
2775
2996
  /**
2776
- * Find a plugin component in a loaded module.
2777
- * Handles: default export, direct key match, multi-plugin .id match.
2997
+ * Find THIS plugin's export in a loaded module — the one lookup both the
2998
+ * component and the device tools use, so a plugin's UI and its tools always
2999
+ * come from the same export. `accept` says what the caller needs from it.
3000
+ * Handles: direct key match, PascalCase key, default export, multi-plugin .id match.
2778
3001
  */
2779
- getComponentFromMemory(pluginId, bundleUrl) {
2780
- const mod = this.modules.get(bundleUrl);
3002
+ findPluginExport(mod, pluginId, accept) {
2781
3003
  if (!mod) return null;
2782
3004
  const shortName = pluginId.includes(":") ? pluginId.split(":").pop() : pluginId;
2783
- if (mod[pluginId] && typeof mod[pluginId].Component === "function") {
2784
- return mod[pluginId];
2785
- }
3005
+ if (accept(mod[pluginId])) return mod[pluginId];
2786
3006
  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
- }
3007
+ if (accept(mod[pascal])) return mod[pascal];
3008
+ if (accept(mod.default)) return mod.default;
2793
3009
  for (const key of Object.keys(mod)) {
2794
3010
  const val = mod[key];
2795
- if (val && typeof val === "object" && val.id === shortName && typeof val.Component === "function") {
2796
- return val;
2797
- }
3011
+ if (val && (val.id === pluginId || val.id === shortName) && accept(val)) return val;
2798
3012
  }
2799
3013
  return null;
2800
3014
  }
@@ -2853,7 +3067,8 @@ async function ensureDeviceToolsLoaded({ force = false } = {}) {
2853
3067
  m.id,
2854
3068
  bundleUrl,
2855
3069
  m.version || "1.0.0",
2856
- getNativeBundleHash(m) || void 0
3070
+ getNativeBundleHash(m) || void 0,
3071
+ resolvePluginGrants({ pluginId: m.id, manifest: m })
2857
3072
  );
2858
3073
  } catch (err) {
2859
3074
  console.warn(`[deviceTools] ${m.id} failed to load:`, err?.message || String(err));
@@ -2885,10 +3100,16 @@ async function loadFromDiskCache() {
2885
3100
  if (skipped > 0) {
2886
3101
  console.log(`[deviceTools] disk-cache recovery: ${mine.length} for "${tenant}", skipped ${skipped} not owned by it`);
2887
3102
  }
2888
- if (mine.length === 0) return [];
2889
- await Promise.all(mine.map(async (e) => {
3103
+ const declared = mine.filter((e) => !!e.trusted?.pluginId);
3104
+ if (declared.length < mine.length) {
3105
+ console.log(
3106
+ `[deviceTools] disk-cache recovery: skipped ${mine.length - declared.length} with no recorded manifest grant (fail closed)`
3107
+ );
3108
+ }
3109
+ if (declared.length === 0) return [];
3110
+ await Promise.all(declared.map(async (e) => {
2890
3111
  try {
2891
- await bundleManager.loadTools("cache:" + e.bundleUrl, e.bundleUrl, e.version, e.bundleHash);
3112
+ await bundleManager.loadTools(e.trusted.pluginId, e.bundleUrl, e.version, e.bundleHash, e.trusted);
2892
3113
  } catch {
2893
3114
  }
2894
3115
  }));
@@ -2909,6 +3130,7 @@ var init_loader = __esm({
2909
3130
  init_bundleManager();
2910
3131
  init_registry();
2911
3132
  init_storage();
3133
+ init_pluginRuntime();
2912
3134
  import_core10 = require("@ateam-ai-mobile/core");
2913
3135
  inflight = null;
2914
3136
  loadedOnce = false;
@@ -2991,6 +3213,7 @@ __export(index_exports, {
2991
3213
  registerService: () => registerService,
2992
3214
  registerTools: () => registerTools,
2993
3215
  resetDeviceToolLoader: () => resetDeviceToolLoader,
3216
+ resolvePluginGrants: () => resolvePluginGrants,
2994
3217
  resolvePluginRuntimeProps: () => resolvePluginRuntimeProps,
2995
3218
  resolvePluginUrl: () => resolvePluginUrl,
2996
3219
  setBundleStorage: () => setBundleStorage,
@@ -3073,109 +3296,9 @@ function explainCapability(status) {
3073
3296
 
3074
3297
  // src/index.ts
3075
3298
  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
3299
+ init_voiceHost();
3159
3300
  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
3301
+ init_pluginRuntime();
3179
3302
  init_registry();
3180
3303
  init_loader();
3181
3304
 
@@ -3293,6 +3416,7 @@ init_native();
3293
3416
  var import_react11 = require("react");
3294
3417
  init_bundleManager();
3295
3418
  init_host();
3419
+ init_pluginRuntime();
3296
3420
  function extractBundleUrl(pluginId, manifest) {
3297
3421
  const render = manifest.render;
3298
3422
  if (!render || typeof render !== "object") return { url: null, source: "none" };
@@ -3330,6 +3454,8 @@ function usePlugin(pluginId, manifest) {
3330
3454
  const bundleHash = extractBundleHash(manifest);
3331
3455
  const componentId = extractComponentId(manifest);
3332
3456
  const lookupId = componentId || pluginId;
3457
+ const grants = resolvePluginGrants({ pluginId, manifest });
3458
+ const grantsKey = JSON.stringify(grants);
3333
3459
  if (ignoredUrl) {
3334
3460
  console.warn(`[usePlugin] ${pluginId}: IGNORED connector-injected bundleUrl="${ignoredUrl}". The host derives bundle URLs from pluginId via the canonical route.`);
3335
3461
  }
@@ -3352,7 +3478,7 @@ function usePlugin(pluginId, manifest) {
3352
3478
  }
3353
3479
  if (bundleUrl) {
3354
3480
  try {
3355
- const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash);
3481
+ const component = await bundleManager.load(pluginId, bundleUrl, version, bundleHash, grants);
3356
3482
  if (cancelled) return;
3357
3483
  setPlugin(component);
3358
3484
  setLoading(false);
@@ -3389,7 +3515,7 @@ function usePlugin(pluginId, manifest) {
3389
3515
  return () => {
3390
3516
  cancelled = true;
3391
3517
  };
3392
- }, [pluginId, lookupId, bundleUrl, bundleHash, version]);
3518
+ }, [pluginId, lookupId, bundleUrl, bundleHash, version, grantsKey]);
3393
3519
  return { plugin, loading, error, bundleMissing };
3394
3520
  }
3395
3521
 
@@ -3546,6 +3672,7 @@ init_SurfaceDispatcher();
3546
3672
  registerService,
3547
3673
  registerTools,
3548
3674
  resetDeviceToolLoader,
3675
+ resolvePluginGrants,
3549
3676
  resolvePluginRuntimeProps,
3550
3677
  resolvePluginUrl,
3551
3678
  setBundleStorage,