@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/{chunk-PKDQLNBL.mjs → chunk-RXR6M623.mjs} +269 -57
- package/dist/chunk-RXR6M623.mjs.map +1 -0
- package/dist/index.d.mts +356 -36
- package/dist/index.d.ts +356 -36
- package/dist/index.js +288 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -101
- package/dist/index.mjs.map +1 -1
- package/dist/{loader-5LUVXUY2.mjs → loader-UUOPZQSI.mjs} +2 -2
- package/package.json +2 -2
- package/src/bundles/bundleManager.ts +161 -57
- package/src/bundles/storage.ts +19 -0
- package/src/deviceStateHost.ts +25 -0
- package/src/hooks/usePlugin.ts +13 -3
- package/src/host.ts +2 -0
- package/src/index.ts +11 -6
- package/src/pluginRuntime.ts +22 -11
- package/src/sdk/device-state.ts +100 -10
- package/src/sdk/index.ts +7 -0
- package/src/sdk/registration.ts +2 -1
- package/src/sdk/types.ts +149 -5
- package/src/sdk/voice.ts +38 -0
- package/src/tools/loader.ts +24 -6
- package/src/tools/registry.ts +12 -5
- package/src/voiceHost.ts +2 -0
- package/dist/chunk-PKDQLNBL.mjs.map +0 -1
- /package/dist/{loader-5LUVXUY2.mjs.map → loader-UUOPZQSI.mjs.map} +0 -0
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
|
-
|
|
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
|
-
...
|
|
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
|
-
/**
|
|
2447
|
-
*
|
|
2448
|
-
*
|
|
2449
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
|
2777
|
-
*
|
|
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
|
-
|
|
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]
|
|
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]
|
|
2788
|
-
|
|
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 &&
|
|
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
|
-
|
|
2889
|
-
|
|
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(
|
|
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,
|