@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.
@@ -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;