@ateam-ai-mobile/plugin-runtime 0.1.5 → 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-RXR6M623.mjs +3085 -0
- package/dist/chunk-RXR6M623.mjs.map +1 -0
- package/dist/index.d.mts +2083 -12
- package/dist/index.d.ts +2083 -12
- package/dist/index.js +2605 -1332
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +265 -2157
- package/dist/index.mjs.map +1 -1
- package/dist/loader-UUOPZQSI.mjs +9 -0
- package/dist/loader-UUOPZQSI.mjs.map +1 -0
- package/package.json +2 -2
- package/src/bundles/bundleManager.ts +496 -51
- package/src/bundles/integrity.ts +29 -0
- package/src/bundles/storage.ts +59 -0
- package/src/deviceStateHost.ts +222 -0
- package/src/hooks/usePlugin.ts +14 -4
- package/src/host.ts +82 -8
- package/src/index.ts +96 -2
- package/src/pluginRuntime.ts +108 -0
- package/src/sdk/CameraPreview.tsx +94 -0
- package/src/sdk/device-state.ts +1315 -0
- package/src/sdk/index.ts +42 -3
- package/src/sdk/registration.ts +40 -0
- package/src/sdk/types.ts +489 -8
- package/src/sdk/voice.ts +186 -0
- package/src/tools/loader.ts +185 -0
- package/src/tools/platformTools.ts +41 -0
- package/src/tools/registry.ts +190 -0
- package/src/tools/serverCalls.ts +78 -0
- package/src/tools/sessionSync.ts +107 -0
- package/src/voiceHost.ts +128 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE runtime contract that turns a plugin's declarations into its gated
|
|
3
|
+
* device surface — `props.device` / `props.native` (L2) and `props.deviceState`
|
|
4
|
+
* (L3) — for EVERY renderer, on EVERY surface, and for its device tools.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. Each renderer used to resolve this itself: the embedded route
|
|
7
|
+
* gated both `native` and `deviceState`; the drawer/fullscreen renderer gated
|
|
8
|
+
* only `native` and read `manifest.native` instead of `manifest.capabilities`;
|
|
9
|
+
* the host-built-in surfaces gated `native` off the registry entry and passed no
|
|
10
|
+
* `deviceState` at all. The SAME plugin therefore got full device access on one
|
|
11
|
+
* surface and `undefined` on another — invisible to the author, whose manifest
|
|
12
|
+
* was correct. A seam owned by three renderers is tested by none. This makes it
|
|
13
|
+
* owned by one: every renderer calls resolvePluginRuntimeProps, so they cannot
|
|
14
|
+
* disagree, and a parity test on this function covers all of them at once.
|
|
15
|
+
*/
|
|
16
|
+
import { getPluginNative, gatePluginNative } from './host';
|
|
17
|
+
import { gateDeviceState } from './deviceStateHost';
|
|
18
|
+
import { gateVoice } from './voiceHost';
|
|
19
|
+
import type { PluginNative, NativeCapabilities } from './sdk/types';
|
|
20
|
+
import type { DeviceState, DeviceBridgeDomain } from './sdk/device-state';
|
|
21
|
+
import type { VoiceAPI } from './sdk/voice';
|
|
22
|
+
|
|
23
|
+
/** The gated surface a plugin Component receives. `device` and `native` are the
|
|
24
|
+
* SAME object (native is the deprecated alias). */
|
|
25
|
+
export interface PluginRuntimeProps {
|
|
26
|
+
device: PluginNative;
|
|
27
|
+
native: PluginNative;
|
|
28
|
+
deviceState: DeviceState;
|
|
29
|
+
voice: VoiceAPI;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One declaration source — either the server manifest or the registry entry. */
|
|
33
|
+
interface PluginDeclaration {
|
|
34
|
+
capabilities?: NativeCapabilities | Record<string, unknown>;
|
|
35
|
+
native?: NativeCapabilities;
|
|
36
|
+
stateDomains?: readonly DeviceBridgeDomain[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PluginRuntimeSource {
|
|
40
|
+
pluginId: string;
|
|
41
|
+
/**
|
|
42
|
+
* The server manifest (Core-passed) — the TRUSTED authority for a REMOTE
|
|
43
|
+
* plugin. `capabilities` is the current field Core emits; `native` is the
|
|
44
|
+
* legacy one. A grant absent here is NOT GRANTED (fail closed).
|
|
45
|
+
*/
|
|
46
|
+
manifest?: PluginDeclaration | null;
|
|
47
|
+
/**
|
|
48
|
+
* The registry entry — the trusted authority for a HOST-OWNED plugin
|
|
49
|
+
* (registered locally, no server manifest). For a REMOTE plugin this is the
|
|
50
|
+
* downloaded bundle's own claim and is UNTRUSTED for gating; it is used ONLY
|
|
51
|
+
* when there is no manifest.
|
|
52
|
+
*/
|
|
53
|
+
plugin?: PluginDeclaration | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Union the recognized native-capability flags from a source's `native`
|
|
57
|
+
* (legacy) and `capabilities` (current) fields. A grant in EITHER is honored;
|
|
58
|
+
* neither shadows the other. `capabilities` may also carry non-capability
|
|
59
|
+
* metadata (e.g. `device_tools`) — gatePluginNative reads only the flags it
|
|
60
|
+
* knows and ignores the rest. */
|
|
61
|
+
function mergeCaps(decl: PluginDeclaration | null | undefined): NativeCapabilities | undefined {
|
|
62
|
+
if (!decl) return undefined;
|
|
63
|
+
return { ...(decl.native as any), ...(decl.capabilities as any) } as NativeCapabilities;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What a plugin is GRANTED — resolved from its one trusted source. The same
|
|
67
|
+
* value gates its rendered surface and its device tools (the tool registry
|
|
68
|
+
* receives it through bundleManager.load/loadTools). */
|
|
69
|
+
export interface PluginGrants {
|
|
70
|
+
capabilities?: NativeCapabilities;
|
|
71
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a plugin's grants — the SINGLE place this happens, for every rendered
|
|
76
|
+
* surface AND for the device tools its bundle registers, so a plugin gets
|
|
77
|
+
* identical access through either.
|
|
78
|
+
*
|
|
79
|
+
* TRUST BOUNDARY. Exactly ONE source is authoritative: the server `manifest` for
|
|
80
|
+
* a remote plugin, the registry `plugin` for a host-owned one. They are NEVER
|
|
81
|
+
* mixed, and a remote plugin's own downloaded bundle claim NEVER expands its
|
|
82
|
+
* capabilities or L3 domains past what the trusted source granted. If the
|
|
83
|
+
* trusted source omits a grant, it is not granted — fail closed. (If Core does
|
|
84
|
+
* not carry `stateDomains` for a remote plugin, the fix is Core passing it, not
|
|
85
|
+
* trusting the bundle: Core already holds the solution's declaration.)
|
|
86
|
+
*/
|
|
87
|
+
export function resolvePluginGrants(src: PluginRuntimeSource): PluginGrants {
|
|
88
|
+
const trusted = src.manifest ?? src.plugin ?? null;
|
|
89
|
+
// Copied, so the grant cannot change after the fact by mutating the source.
|
|
90
|
+
const stateDomains = trusted?.stateDomains ? [...trusted.stateDomains] : undefined;
|
|
91
|
+
return { capabilities: mergeCaps(trusted), stateDomains };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Turn a plugin's grants into the gated { device, native, deviceState, voice } it
|
|
95
|
+
* receives on every rendered surface. */
|
|
96
|
+
export function resolvePluginRuntimeProps(src: PluginRuntimeSource): PluginRuntimeProps {
|
|
97
|
+
const { capabilities, stateDomains } = resolvePluginGrants(src);
|
|
98
|
+
const native = gatePluginNative(getPluginNative(), capabilities, src.pluginId);
|
|
99
|
+
const deviceState = gateDeviceState(stateDomains);
|
|
100
|
+
// Voice is a capability flag, gated the same way — the TRUSTED declaration
|
|
101
|
+
// decides, a downloaded bundle cannot grant itself the session (or the
|
|
102
|
+
// transcript it can read). The facade is bound to the TRUSTED pluginId, so the
|
|
103
|
+
// session is isolated per solution: declaring voice runs YOUR conversation, it
|
|
104
|
+
// does not attach to another plugin's live one.
|
|
105
|
+
const voice = gateVoice(!!(capabilities as any)?.voice, src.pluginId);
|
|
106
|
+
// device and native are intentionally the SAME gated object (R4 rename).
|
|
107
|
+
return { device: native, native, deviceState, voice };
|
|
108
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CameraPreview — the plugin-facing way to SHOW the user what the camera sees.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS RATHER THAN HANDING PLUGINS `expo-camera`.
|
|
5
|
+
*
|
|
6
|
+
* The design has exactly two doors to the device: `props.native.*` (mechanical
|
|
7
|
+
* verbs, capability-gated, throws) and `props.deviceState.*` (semantic state,
|
|
8
|
+
* gated, says unknown). Registering a raw native module for bundles would open
|
|
9
|
+
* a THIRD door with no grammar, no gate and no honest failure mode — a plugin
|
|
10
|
+
* could mount a camera without declaring it, and nothing in the manifest would
|
|
11
|
+
* say so. That is the shape of hole this platform keeps closing, not opening.
|
|
12
|
+
*
|
|
13
|
+
* So the preview is a COMPONENT the plugin composes, and the pixels stay on the
|
|
14
|
+
* host's side of the boundary. A plugin can show the camera; it still cannot
|
|
15
|
+
* touch a frame. Reading frames remains `deviceState.visual` (semantic, with
|
|
16
|
+
* freshness) and analysing them remains `native.vision` (which takes an id and
|
|
17
|
+
* resolves it inside the trusted layer).
|
|
18
|
+
*
|
|
19
|
+
* IT IS GATED BY THE SAME FLAG AS `native.camera`. One door, one lock: a plugin
|
|
20
|
+
* that has not declared `capabilities: { camera: true }` gets a refusal panel
|
|
21
|
+
* saying exactly that, not a black rectangle it might mistake for a dark room.
|
|
22
|
+
*
|
|
23
|
+
* IT NEVER RENDERS NOTHING. Every failure — capability not declared, host has
|
|
24
|
+
* not installed a provider, OS permission refused — draws a panel stating which
|
|
25
|
+
* one. A preview that silently renders empty is indistinguishable from a camera
|
|
26
|
+
* pointed at a wall, and a guide built on it would describe a scene nobody saw.
|
|
27
|
+
*/
|
|
28
|
+
import React from 'react';
|
|
29
|
+
import { View, Text } from './primitives';
|
|
30
|
+
import type { PluginNative } from './types';
|
|
31
|
+
import { getCameraPreviewProvider } from '../host';
|
|
32
|
+
|
|
33
|
+
export interface CameraPreviewProps {
|
|
34
|
+
/**
|
|
35
|
+
* The plugin's own `props.native`. REQUIRED, and it is the gate: the runtime
|
|
36
|
+
* stamps the declared capabilities onto this object, so passing it is how the
|
|
37
|
+
* component knows whether this plugin may show a camera at all.
|
|
38
|
+
*/
|
|
39
|
+
native: PluginNative;
|
|
40
|
+
facing?: 'front' | 'back';
|
|
41
|
+
style?: any;
|
|
42
|
+
/** Rendered instead of the built-in panel when the preview is unavailable. */
|
|
43
|
+
renderUnavailable?: (reason: string) => React.ReactElement | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Declared capabilities, stamped onto the gated object by gatePluginNative. */
|
|
47
|
+
function declaredCapabilities(native: any): Record<string, boolean> | null {
|
|
48
|
+
return (native && native.__declaredCapabilities) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function CameraPreview({ native, facing = 'back', style, renderUnavailable }: CameraPreviewProps) {
|
|
52
|
+
const caps = declaredCapabilities(native);
|
|
53
|
+
|
|
54
|
+
// The gate. Deliberately checked BEFORE asking the host for a provider, so a
|
|
55
|
+
// plugin cannot learn whether a camera exists without declaring that it wants
|
|
56
|
+
// one — an undeclared plugin gets the same answer on every device.
|
|
57
|
+
if (caps && !caps.camera) {
|
|
58
|
+
return unavailable(
|
|
59
|
+
"this plugin did not declare `capabilities: { camera: true }`, so it may not show the camera",
|
|
60
|
+
renderUnavailable,
|
|
61
|
+
style,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const Provider = getCameraPreviewProvider();
|
|
66
|
+
if (!Provider) {
|
|
67
|
+
// The host has not installed a preview implementation. Say so rather than
|
|
68
|
+
// rendering black: "no provider" and "a dark room" must never look alike.
|
|
69
|
+
return unavailable(
|
|
70
|
+
'this app does not provide a camera preview (the host has not called setCameraPreviewProvider)',
|
|
71
|
+
renderUnavailable,
|
|
72
|
+
style,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return <Provider facing={facing} style={style} renderUnavailable={renderUnavailable} />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function unavailable(
|
|
80
|
+
reason: string,
|
|
81
|
+
render: CameraPreviewProps['renderUnavailable'],
|
|
82
|
+
style: any,
|
|
83
|
+
): React.ReactElement | null {
|
|
84
|
+
if (render) return render(reason);
|
|
85
|
+
return (
|
|
86
|
+
<View style={[{ flex: 1, backgroundColor: '#000', alignItems: 'center', justifyContent: 'center', padding: 20 }, style]}>
|
|
87
|
+
<Text style={{ color: '#39FF66', fontSize: 12, textAlign: 'center' }}>
|
|
88
|
+
{`NO CAMERA PREVIEW\n${reason}`}
|
|
89
|
+
</Text>
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default CameraPreview;
|