@ateam-ai-mobile/plugin-runtime 0.1.5 → 0.1.7
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 +2873 -0
- package/dist/chunk-PKDQLNBL.mjs.map +1 -0
- package/dist/index.d.mts +1759 -8
- package/dist/index.d.ts +1759 -8
- package/dist/index.js +2467 -1321
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +335 -2139
- package/dist/index.mjs.map +1 -1
- package/dist/loader-5LUVXUY2.mjs +9 -0
- package/dist/loader-5LUVXUY2.mjs.map +1 -0
- package/package.json +2 -2
- package/src/bundles/bundleManager.ts +368 -27
- package/src/bundles/integrity.ts +29 -0
- package/src/bundles/storage.ts +40 -0
- package/src/deviceStateHost.ts +197 -0
- package/src/hooks/usePlugin.ts +1 -1
- package/src/host.ts +80 -8
- package/src/index.ts +90 -1
- package/src/pluginRuntime.ts +97 -0
- package/src/sdk/CameraPreview.tsx +94 -0
- package/src/sdk/device-state.ts +1225 -0
- package/src/sdk/index.ts +35 -3
- package/src/sdk/registration.ts +39 -0
- package/src/sdk/types.ts +345 -8
- package/src/sdk/voice.ts +148 -0
- package/src/tools/loader.ts +167 -0
- package/src/tools/platformTools.ts +41 -0
- package/src/tools/registry.ts +183 -0
- package/src/tools/serverCalls.ts +78 -0
- package/src/tools/sessionSync.ts +107 -0
- package/src/voiceHost.ts +126 -0
package/src/sdk/index.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Registration API
|
|
8
|
-
export { register, registerService } from './registration';
|
|
9
|
-
import { register, registerService } from './registration';
|
|
10
|
-
export const PluginSDK = { register, registerService } as const;
|
|
8
|
+
export { register, registerService, registerTools } from './registration';
|
|
9
|
+
import { register, registerService, registerTools } from './registration';
|
|
10
|
+
export const PluginSDK = { register, registerService, registerTools } as const;
|
|
11
11
|
|
|
12
12
|
// Types
|
|
13
13
|
export type {
|
|
@@ -24,9 +24,17 @@ export type {
|
|
|
24
24
|
ConnectivityAPI,
|
|
25
25
|
FileSystemAPI,
|
|
26
26
|
CookiesAPI,
|
|
27
|
+
WebViewCookie,
|
|
27
28
|
SharingAPI,
|
|
28
29
|
ScannerAPI,
|
|
29
30
|
NfcAPI,
|
|
31
|
+
BluetoothAPI,
|
|
32
|
+
BluetoothDevice,
|
|
33
|
+
VisionAPI,
|
|
34
|
+
VisionText,
|
|
35
|
+
VisionLabel,
|
|
36
|
+
VisionObject,
|
|
37
|
+
VisionFrameQuality,
|
|
30
38
|
BiometricsAPI,
|
|
31
39
|
BiometricsAPIv2,
|
|
32
40
|
NativeCapabilities,
|
|
@@ -36,8 +44,28 @@ export type {
|
|
|
36
44
|
RegisteredPlugin,
|
|
37
45
|
RegisteredUIPlugin,
|
|
38
46
|
RegisteredServicePlugin,
|
|
47
|
+
RegisteredToolsPlugin,
|
|
48
|
+
DeviceToolContext,
|
|
49
|
+
DeviceToolDefinition,
|
|
50
|
+
DeviceToolFn,
|
|
39
51
|
} from './types';
|
|
40
52
|
|
|
53
|
+
// Voice — the live conversational SESSION surface (props.voice).
|
|
54
|
+
export type {
|
|
55
|
+
VoiceAPI,
|
|
56
|
+
VoiceState,
|
|
57
|
+
VoiceSnapshot,
|
|
58
|
+
VoiceTranscriptEntry,
|
|
59
|
+
VoiceStartOptions,
|
|
60
|
+
VoiceEvent,
|
|
61
|
+
VoiceSubscription,
|
|
62
|
+
VoiceCaller,
|
|
63
|
+
VoiceSessionProvider,
|
|
64
|
+
} from './voice';
|
|
65
|
+
// The privileged platform caller (a value) — the host injects the owner-aware
|
|
66
|
+
// provider and its own surfaces call it as this caller. See VoiceSessionProvider.
|
|
67
|
+
export { PLATFORM_VOICE_CALLER } from './voice';
|
|
68
|
+
|
|
41
69
|
// RN primitives (plugins import from the SDK, not from react-native)
|
|
42
70
|
export {
|
|
43
71
|
View,
|
|
@@ -72,5 +100,9 @@ export {
|
|
|
72
100
|
} from './components';
|
|
73
101
|
|
|
74
102
|
// Theme + API hooks
|
|
103
|
+
// Showing the camera is a COMPONENT, not a native module handed to bundles —
|
|
104
|
+
// one door, one lock. See CameraPreview.tsx for why.
|
|
105
|
+
export { CameraPreview } from './CameraPreview';
|
|
106
|
+
export type { CameraPreviewProps } from './CameraPreview';
|
|
75
107
|
export { usePluginTheme, setPluginThemeColors } from './usePluginTheme';
|
|
76
108
|
export { useApi } from './useApi';
|
package/src/sdk/registration.ts
CHANGED
|
@@ -3,29 +3,68 @@
|
|
|
3
3
|
* Solution developers use these to register their plugins.
|
|
4
4
|
*/
|
|
5
5
|
import type {
|
|
6
|
+
DeviceToolDefinition,
|
|
6
7
|
NativeCapabilities,
|
|
7
8
|
PluginHooks,
|
|
8
9
|
PluginProps,
|
|
9
10
|
RegisteredUIPlugin,
|
|
10
11
|
RegisteredServicePlugin,
|
|
12
|
+
RegisteredToolsPlugin,
|
|
11
13
|
} from './types';
|
|
14
|
+
import type { DeviceBridgeDomain } from './device-state';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Register a UI or hybrid plugin (has a visual component).
|
|
18
|
+
*
|
|
19
|
+
* `tools` is optional: a plugin may ship UI and local tools together. Tools are
|
|
20
|
+
* picked up when the bundle loads, independently of whether the UI is mounted.
|
|
15
21
|
*/
|
|
16
22
|
export function register(
|
|
17
23
|
id: string,
|
|
18
24
|
config: {
|
|
19
25
|
type?: 'ui' | 'hybrid';
|
|
20
26
|
capabilities?: NativeCapabilities;
|
|
27
|
+
/** L3 DeviceState domains this plugin's UI reads via props.deviceState (e.g.
|
|
28
|
+
* ['spatial','visual']). Carried through so a HOST-registered plugin (no
|
|
29
|
+
* server manifest) can declare its domains — resolvePluginRuntimeProps reads
|
|
30
|
+
* this as the trusted source when there is no manifest. Dropping it here left
|
|
31
|
+
* every registered UI plugin with no L3, silently. Mirrors registerTools. */
|
|
32
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
21
33
|
Component: React.FC<PluginProps>;
|
|
34
|
+
tools?: DeviceToolDefinition[];
|
|
22
35
|
},
|
|
23
36
|
): RegisteredUIPlugin {
|
|
24
37
|
return {
|
|
25
38
|
id,
|
|
26
39
|
type: config.type || 'ui',
|
|
27
40
|
capabilities: config.capabilities,
|
|
41
|
+
stateDomains: config.stateDomains,
|
|
28
42
|
Component: config.Component,
|
|
43
|
+
tools: config.tools,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register a tools-only plugin — a `tools/` section with no UI at all.
|
|
49
|
+
*
|
|
50
|
+
* Nothing here mounts. The tools become callable as soon as the bundle is
|
|
51
|
+
* evaluated, and stay callable with nothing of the plugin on screen.
|
|
52
|
+
*/
|
|
53
|
+
export function registerTools(
|
|
54
|
+
id: string,
|
|
55
|
+
config: {
|
|
56
|
+
capabilities?: NativeCapabilities;
|
|
57
|
+
/** L3 DeviceState domains the tools may read via ctx.deviceState (e.g. ['visual']). */
|
|
58
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
59
|
+
tools: DeviceToolDefinition[];
|
|
60
|
+
},
|
|
61
|
+
): RegisteredToolsPlugin {
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
type: 'tools',
|
|
65
|
+
capabilities: config.capabilities,
|
|
66
|
+
stateDomains: config.stateDomains,
|
|
67
|
+
tools: config.tools,
|
|
29
68
|
};
|
|
30
69
|
}
|
|
31
70
|
|
package/src/sdk/types.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* This is the developer contract for native mobile plugins.
|
|
5
5
|
* Both UI and service plugins use these interfaces.
|
|
6
6
|
*/
|
|
7
|
+
import type { DeviceState, DeviceBridgeDomain } from './device-state';
|
|
8
|
+
import type { VoiceAPI } from './voice';
|
|
7
9
|
|
|
8
10
|
// ── Plugin Context ──────────────────────────────────────────
|
|
9
11
|
|
|
@@ -55,8 +57,27 @@ export interface PluginBridge {
|
|
|
55
57
|
|
|
56
58
|
export interface CameraAPI {
|
|
57
59
|
takePhoto(opts?: { quality?: number; facing?: 'front' | 'back' }): Promise<{ uri: string; width: number; height: number }>;
|
|
60
|
+
/**
|
|
61
|
+
* @deprecated Use `native.scanner.scanOnce()`. Scanning needs a MOUNTED
|
|
62
|
+
* camera, which a bare function call cannot provide; this refuses by name
|
|
63
|
+
* and points there.
|
|
64
|
+
*/
|
|
58
65
|
scanBarcode(opts?: { formats?: string[] }): Promise<{ data: string; format: string }>;
|
|
59
66
|
pickImage(opts?: { multiple?: boolean }): Promise<{ uri: string }[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Record a video clip the user deliberately makes — a damage claim, a
|
|
69
|
+
* how-it-sounds clip, something to attach.
|
|
70
|
+
*
|
|
71
|
+
* NOT the Visual domain. That is an ephemeral frame ring a reasoning loop
|
|
72
|
+
* reads and nobody keeps; this produces a FILE. Different jobs, and the
|
|
73
|
+
* grammar says so: this is a verb that throws, Visual is a state read that
|
|
74
|
+
* says `unknown`.
|
|
75
|
+
*
|
|
76
|
+
* Rides the same mounted camera surface as `scanner.scanOnce`, and throws
|
|
77
|
+
* the same coded failures. `withAudio` needs microphone permission and
|
|
78
|
+
* REFUSES without it rather than quietly recording silence.
|
|
79
|
+
*/
|
|
80
|
+
recordVideo(opts?: { maxDurationMs?: number; withAudio?: boolean }): Promise<{ uri: string; durationMs: number | null }>;
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
export interface LocationAPI {
|
|
@@ -88,6 +109,16 @@ export interface HapticsAPI {
|
|
|
88
109
|
impact(style: 'light' | 'medium' | 'heavy'): void;
|
|
89
110
|
notification(type: 'success' | 'warning' | 'error'): void;
|
|
90
111
|
selection(): void;
|
|
112
|
+
/**
|
|
113
|
+
* Shorthand for notification('error').
|
|
114
|
+
*
|
|
115
|
+
* Documented in the platform guide and already CALLED by shipped plugins
|
|
116
|
+
* (connections-panel), which only compiled because they reached it through
|
|
117
|
+
* optional chaining — `native?.haptics?.error?.()` silently does nothing when
|
|
118
|
+
* the method is absent, so a failure the user was meant to FEEL produced no
|
|
119
|
+
* buzz and no error. Declared here so the call site is checked.
|
|
120
|
+
*/
|
|
121
|
+
error(): void;
|
|
91
122
|
}
|
|
92
123
|
|
|
93
124
|
export interface ConnectivityAPI {
|
|
@@ -139,11 +170,22 @@ export interface ScannerAPI {
|
|
|
139
170
|
/** Prompt the user for camera permission. */
|
|
140
171
|
requestPermissions(): Promise<{ granted: boolean; status: string }>;
|
|
141
172
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
173
|
+
* Show the scanner and resolve with the FIRST code read.
|
|
174
|
+
*
|
|
175
|
+
* The plugin no longer mounts anything: the host renders one
|
|
176
|
+
* `CameraTaskHost`, this raises it, and the promise settles when a code is
|
|
177
|
+
* found. That is why the capture lifecycle is owned in exactly one place
|
|
178
|
+
* instead of by every plugin that wants to scan.
|
|
179
|
+
*
|
|
180
|
+
* THROWS rather than resolving empty, with a code that says which:
|
|
181
|
+
* TIMEOUT nothing was found in time
|
|
182
|
+
* CANCELLED the user dismissed it — a choice, not a fault
|
|
183
|
+
* PERMISSION_DENIED camera permission refused
|
|
184
|
+
* BUSY another camera task is running
|
|
185
|
+
* UNAVAILABLE no camera surface is mounted in this app
|
|
186
|
+
* An empty string would read as a code that scanned to nothing.
|
|
146
187
|
*/
|
|
188
|
+
scanOnce(opts?: { formats?: string[]; timeoutMs?: number }): Promise<{ data: string; format: string }>;
|
|
147
189
|
}
|
|
148
190
|
|
|
149
191
|
// ── NFC API (tag read) ────────────────────────────────────
|
|
@@ -171,10 +213,171 @@ export interface BiometricsAPIv2 {
|
|
|
171
213
|
}
|
|
172
214
|
|
|
173
215
|
|
|
216
|
+
// ── Bluetooth / BLE ───────────────────────────────────────
|
|
217
|
+
//
|
|
218
|
+
// SHAPE ONLY — every method throws CapabilityUnavailableError in this build.
|
|
219
|
+
// It is declared anyway, and that is the point: `bluetooth` was already a
|
|
220
|
+
// manifest flag with nothing behind it, so a plugin could declare it, pass the
|
|
221
|
+
// gate, and reach `undefined`. Calling `native.bluetooth.scan()` then failed
|
|
222
|
+
// with "cannot read properties of undefined" — a crash that tells the author
|
|
223
|
+
// nothing and a model less than nothing.
|
|
224
|
+
//
|
|
225
|
+
// A declared-but-unbuilt capability must FAIL BY NAME. That is the whole rule:
|
|
226
|
+
// absence is an answer, but only when it says what is absent.
|
|
227
|
+
//
|
|
228
|
+
// WHY IT IS KEPT RATHER THAN DROPPED: BLE beacons are one of the few indoor
|
|
229
|
+
// position signals that work where GPS does not, which is exactly the case Ada
|
|
230
|
+
// Guide runs into the moment the user walks into a building. Also pairing,
|
|
231
|
+
// loyalty tags, and proximity triggers. The shape is here so a solution can be
|
|
232
|
+
// designed against it before the native side exists.
|
|
233
|
+
//
|
|
234
|
+
// IMPLEMENTING IT is a real project, not a wiring job: react-native-ble-plx,
|
|
235
|
+
// iOS background modes, NSBluetoothAlwaysUsageDescription, and a permission
|
|
236
|
+
// flow of its own.
|
|
237
|
+
export interface BluetoothDevice {
|
|
238
|
+
id: string;
|
|
239
|
+
name: string | null;
|
|
240
|
+
/** Signal strength in dBm. Null when not reported — never 0, which is a
|
|
241
|
+
* legitimate (and very strong) reading. */
|
|
242
|
+
rssi: number | null;
|
|
243
|
+
/** Manufacturer/service payload, if the advertisement carried one. */
|
|
244
|
+
serviceUUIDs?: string[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Vision — on-device image understanding ────────────────
|
|
248
|
+
//
|
|
249
|
+
// SHAPE ONLY. Every method refuses by name until a native module lands.
|
|
250
|
+
//
|
|
251
|
+
// WHY IT IS HERE AND NOT IN THE VISUAL DOMAIN. The freeze is explicit
|
|
252
|
+
// (device-state.ts §3): "Visual is NOT the camera API… Everything interpretive
|
|
253
|
+
// — what is in the scene, what the sign says, which corridor to take — sits
|
|
254
|
+
// ABOVE Device Bridge and is not modelled here." Visual owns the frame
|
|
255
|
+
// TIMELINE; deciding what a frame MEANS is this layer's job. Two different
|
|
256
|
+
// questions, deliberately kept apart:
|
|
257
|
+
//
|
|
258
|
+
// deviceState.visual.getSnapshot() → "is there a current frame, how old"
|
|
259
|
+
// native.vision.readText(frame) → "what does the sign say"
|
|
260
|
+
//
|
|
261
|
+
// FRAMES NEVER LEAVE THE PHONE. That is Arie's decision (2026-08-15) and it is
|
|
262
|
+
// the whole reason this is a platform capability rather than an API call from
|
|
263
|
+
// a plugin: analysis runs on-device, so there is no per-frame cost and no image
|
|
264
|
+
// on anyone's server. An implementation that POSTs the frame somewhere would
|
|
265
|
+
// satisfy the types and violate the design.
|
|
266
|
+
//
|
|
267
|
+
// IT TAKES A HANDLE, NOT PIXELS. `MediaHandle.id` from a VisualFrameRef — the
|
|
268
|
+
// core rule that media crosses by reference. A plugin never holds bytes.
|
|
269
|
+
//
|
|
270
|
+
// NATIVE SIDE (for whoever implements it): iOS Vision —
|
|
271
|
+
// VNRecognizeTextRequest for `readText`, VNClassifyImageRequest for `describe`;
|
|
272
|
+
// Android MLKit text recognition + image labeling. Both are on-device and free
|
|
273
|
+
// per call.
|
|
274
|
+
export interface VisionText {
|
|
275
|
+
/** The recognised string. */
|
|
276
|
+
text: string;
|
|
277
|
+
/** 0..1 recogniser confidence. Null when the platform gives no estimate —
|
|
278
|
+
* never 1, which would read as certainty nothing measured. */
|
|
279
|
+
confidence: number | null;
|
|
280
|
+
/** Normalised 0..1 box in the frame, origin top-left. Lets a guide say
|
|
281
|
+
* "the sign on your right" rather than just quoting words. */
|
|
282
|
+
box?: { x: number; y: number; width: number; height: number };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface VisionLabel {
|
|
286
|
+
/** A label from the platform's classifier vocabulary — NOT free text. */
|
|
287
|
+
label: string;
|
|
288
|
+
confidence: number | null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface VisionObject {
|
|
292
|
+
/** A detector-vocabulary class name (e.g. "Car", "Traffic light", "Person") —
|
|
293
|
+
* NOT free text. */
|
|
294
|
+
label: string;
|
|
295
|
+
/** 0..1 detector confidence. Null when the platform gives no estimate — never
|
|
296
|
+
* 1, which would read as certainty nothing measured. */
|
|
297
|
+
confidence: number | null;
|
|
298
|
+
/** Normalised 0..1 box in the frame, origin top-left. Where the thing is,
|
|
299
|
+
* so a guide can say "on your right" rather than just naming it. */
|
|
300
|
+
box?: { x: number; y: number; width: number; height: number };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* How usable a frame is, as measured rather than guessed.
|
|
305
|
+
*
|
|
306
|
+
* This one is the exception to the split above: it is a MEASUREMENT of the
|
|
307
|
+
* image, not an interpretation of the scene, so its results belong on
|
|
308
|
+
* `VisualFrameRef.quality` — which currently reports `unknown` for all three
|
|
309
|
+
* because nothing looks at the pixels. Wiring this is what lets a guide say
|
|
310
|
+
* "it's too dark for me to tell" instead of describing a black frame.
|
|
311
|
+
*/
|
|
312
|
+
export interface VisionFrameQuality {
|
|
313
|
+
blur: 'sharp' | 'slight' | 'severe' | 'unknown';
|
|
314
|
+
exposure: 'good' | 'dark' | 'bright' | 'unknown';
|
|
315
|
+
/** True when the lens is covered. Null = not assessed. */
|
|
316
|
+
obstructed: boolean | null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface VisionAPI {
|
|
320
|
+
/** Is on-device vision available at all? False is a real answer. */
|
|
321
|
+
isSupported(): Promise<boolean>;
|
|
322
|
+
/**
|
|
323
|
+
* Read text in a frame (OCR).
|
|
324
|
+
*
|
|
325
|
+
* Returns an EMPTY ARRAY when it looked and found no text — that is a real
|
|
326
|
+
* finding, distinct from throwing, which means it could not look.
|
|
327
|
+
*/
|
|
328
|
+
readText(frameId: string, opts?: { languages?: string[] }): Promise<VisionText[]>;
|
|
329
|
+
/**
|
|
330
|
+
* Label what is in a frame.
|
|
331
|
+
*
|
|
332
|
+
* Deliberately labels, not prose. A sentence is the SKILL's job — this layer
|
|
333
|
+
* must not put words in the model's mouth, and a classifier that returns
|
|
334
|
+
* "a bathroom sign ahead on the left" is asserting a spatial relationship it
|
|
335
|
+
* did not measure.
|
|
336
|
+
*/
|
|
337
|
+
describe(frameId: string, opts?: { maxLabels?: number }): Promise<VisionLabel[]>;
|
|
338
|
+
/**
|
|
339
|
+
* Detect the objects in a frame, each with a box.
|
|
340
|
+
*
|
|
341
|
+
* Distinct from `describe`: describe LABELS the whole scene ("kitchen"),
|
|
342
|
+
* detectObjects LOCATES the things in it ("Person" on the left, "Car" ahead) —
|
|
343
|
+
* which is what a guidance loop reads to say where something is. Returns an
|
|
344
|
+
* EMPTY ARRAY when it looked and found nothing, distinct from throwing (could
|
|
345
|
+
* not look). On-device (bundled detector); frames never leave the phone.
|
|
346
|
+
*/
|
|
347
|
+
detectObjects(
|
|
348
|
+
frameId: string,
|
|
349
|
+
opts?: { maxItems?: number; minConfidence?: number },
|
|
350
|
+
): Promise<VisionObject[]>;
|
|
351
|
+
/** Measure whether a frame is usable. Feeds VisualFrameRef.quality. */
|
|
352
|
+
assessQuality(frameId: string): Promise<VisionFrameQuality>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface BluetoothAPI {
|
|
356
|
+
/** Is BLE hardware present and permitted? */
|
|
357
|
+
isSupported(): Promise<boolean>;
|
|
358
|
+
/** Is the radio switched on? (Separate from permission — both can block.) */
|
|
359
|
+
isEnabled(): Promise<boolean>;
|
|
360
|
+
/**
|
|
361
|
+
* Scan for advertising devices for `timeoutMs`, then stop.
|
|
362
|
+
*
|
|
363
|
+
* Bounded rather than open-ended on purpose: a scan left running is a
|
|
364
|
+
* battery drain the caller cannot see, and the mistake is invisible until a
|
|
365
|
+
* user notices their phone is flat.
|
|
366
|
+
*/
|
|
367
|
+
scan(opts?: { timeoutMs?: number; serviceUUIDs?: string[] }): Promise<BluetoothDevice[]>;
|
|
368
|
+
}
|
|
369
|
+
|
|
174
370
|
export interface PluginNative {
|
|
175
371
|
camera: CameraAPI;
|
|
176
372
|
location: LocationAPI;
|
|
177
|
-
|
|
373
|
+
/**
|
|
374
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
375
|
+
* `storageFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
376
|
+
* single shared storage across plugins was a cross-plugin read/write hole;
|
|
377
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
378
|
+
* satisfy the type; new providers implement `storageFor`.
|
|
379
|
+
*/
|
|
380
|
+
storage?: StorageAPI;
|
|
178
381
|
/** @deprecated use `biometricsV2` for the native-backed API. This stub
|
|
179
382
|
* remains for backwards compatibility. */
|
|
180
383
|
biometrics: BiometricsAPI;
|
|
@@ -182,11 +385,41 @@ export interface PluginNative {
|
|
|
182
385
|
notifications: NotificationsAPI;
|
|
183
386
|
haptics: HapticsAPI;
|
|
184
387
|
connectivity: ConnectivityAPI;
|
|
185
|
-
|
|
388
|
+
/**
|
|
389
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
390
|
+
* `fileSystemFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
391
|
+
* single shared filesystem across plugins was a cross-plugin read/write hole;
|
|
392
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
393
|
+
* satisfy the type; new providers implement `fileSystemFor`.
|
|
394
|
+
*/
|
|
395
|
+
fileSystem?: FileSystemAPI;
|
|
396
|
+
/**
|
|
397
|
+
* Per-plugin filesystem: returns a FileSystemAPI whose sandbox root is scoped
|
|
398
|
+
* to THIS plugin (`plugin-files/<pluginId>/`), so one plugin cannot read,
|
|
399
|
+
* write or reach another plugin's files even via traversal. `gatePluginNative`
|
|
400
|
+
* uses it (passing the plugin's id) to isolate each plugin. If it is absent or
|
|
401
|
+
* no id is known, the gate FAILS CLOSED (blocks) rather than fall back to a
|
|
402
|
+
* shared filesystem — there is no shared bucket.
|
|
403
|
+
*/
|
|
404
|
+
fileSystemFor?(pluginId: string): FileSystemAPI;
|
|
405
|
+
/**
|
|
406
|
+
* Per-plugin storage: returns a StorageAPI whose KV namespace AND SQLite
|
|
407
|
+
* database are scoped to THIS plugin, so one plugin cannot read, overwrite,
|
|
408
|
+
* or query another plugin's key-value entries or tables. `gatePluginNative`
|
|
409
|
+
* uses it (passing the plugin's id) to isolate each plugin. Identity is
|
|
410
|
+
* threaded through the TRUSTED provider here — never a caller-supplied
|
|
411
|
+
* key/table prefix. If it is absent or no id is known, the gate FAILS CLOSED
|
|
412
|
+
* (blocks) rather than fall back to a shared store — there is no shared bucket.
|
|
413
|
+
*/
|
|
414
|
+
storageFor?(pluginId: string): StorageAPI;
|
|
186
415
|
cookies: CookiesAPI;
|
|
187
416
|
sharing: SharingAPI;
|
|
188
417
|
scanner: ScannerAPI;
|
|
189
418
|
nfc: NfcAPI;
|
|
419
|
+
/** SHAPE ONLY in this build — see BluetoothAPI. Present so it fails by name. */
|
|
420
|
+
bluetooth: BluetoothAPI;
|
|
421
|
+
/** SHAPE ONLY in this build — see VisionAPI. On-device image understanding. */
|
|
422
|
+
vision: VisionAPI;
|
|
190
423
|
}
|
|
191
424
|
|
|
192
425
|
// ── Native capability flags (manifest declaration) ──────────
|
|
@@ -199,7 +432,17 @@ export interface NativeCapabilities {
|
|
|
199
432
|
backgroundTasks?: boolean;
|
|
200
433
|
fileSystem?: boolean;
|
|
201
434
|
notifications?: boolean;
|
|
435
|
+
/** BLE. Declaring it passes the gate but every call refuses by name until
|
|
436
|
+
* the native side exists — see BluetoothAPI. */
|
|
202
437
|
bluetooth?: boolean;
|
|
438
|
+
/** On-device image understanding (OCR, labels, frame quality). Refuses by
|
|
439
|
+
* name until the native module lands — see VisionAPI. */
|
|
440
|
+
vision?: boolean;
|
|
441
|
+
/** The live voice SESSION (props.voice). A privacy surface — a declaring
|
|
442
|
+
* plugin can read the conversation transcript — so it is gated like any
|
|
443
|
+
* other capability, and coupled to the platform's unsuppressable mic
|
|
444
|
+
* indicator. See VoiceAPI. */
|
|
445
|
+
voice?: boolean;
|
|
203
446
|
haptics?: boolean;
|
|
204
447
|
/** Read the platform cookie jar after a WebView login flow. */
|
|
205
448
|
cookies?: boolean;
|
|
@@ -258,7 +501,43 @@ export interface ThemeTokens {
|
|
|
258
501
|
|
|
259
502
|
export interface PluginProps {
|
|
260
503
|
bridge: PluginBridge;
|
|
261
|
-
|
|
504
|
+
/**
|
|
505
|
+
* L2 Device Primitives — the camera/location/storage/haptics/… surface, gated
|
|
506
|
+
* to the capabilities this plugin declared. This is the canonical name (R4);
|
|
507
|
+
* the underlying L1 native modules keep the `native` name, but what a plugin
|
|
508
|
+
* receives is `props.device`.
|
|
509
|
+
*/
|
|
510
|
+
device: PluginNative;
|
|
511
|
+
/**
|
|
512
|
+
* @deprecated Renamed to `device` in R4. Still passed (the SAME gated object)
|
|
513
|
+
* so existing/remote plugin bundles keep working — migrate to `props.device`.
|
|
514
|
+
*/
|
|
515
|
+
native?: PluginNative;
|
|
516
|
+
/**
|
|
517
|
+
* L3 DeviceState — the SEMANTIC layer (`props.deviceState.spatial/visual/…`),
|
|
518
|
+
* gated to the domains this plugin declared in `stateDomains`. Reads answer
|
|
519
|
+
* `unknown` rather than throwing; a domain the build has no engine for, or the
|
|
520
|
+
* plugin did not declare, returns an explicit refusal.
|
|
521
|
+
*
|
|
522
|
+
* REQUIRED, deliberately: an optional field is exactly the compile-time escape
|
|
523
|
+
* hatch that let a renderer ship without it (props.deviceState === undefined,
|
|
524
|
+
* which crashed on drawer/card). Every renderer builds this through
|
|
525
|
+
* resolvePluginRuntimeProps, so the constructor side always has it; making it
|
|
526
|
+
* required means a NEW renderer that forgets to pass it fails to compile.
|
|
527
|
+
*/
|
|
528
|
+
deviceState: DeviceState;
|
|
529
|
+
/**
|
|
530
|
+
* The live conversational SESSION, gated to plugins that declared
|
|
531
|
+
* `capabilities: { voice: true }`. A SESSION, not a device sensor: start/stop
|
|
532
|
+
* the loop, read + subscribe to the transcript, and speak first via
|
|
533
|
+
* `prompt()`. The mic-active indicator is platform-owned and cannot be
|
|
534
|
+
* suppressed from here.
|
|
535
|
+
*
|
|
536
|
+
* REQUIRED, like `deviceState`: every renderer builds it through
|
|
537
|
+
* resolvePluginRuntimeProps, so the object is always present. An undeclared
|
|
538
|
+
* plugin gets a refusal (reads say so, actions throw) — never a fake `idle`.
|
|
539
|
+
*/
|
|
540
|
+
voice: VoiceAPI;
|
|
262
541
|
theme: ThemeTokens;
|
|
263
542
|
context: PluginContext;
|
|
264
543
|
/** Engine-supplied props from `sys.focusUiPlugin(pluginId, props)`.
|
|
@@ -290,6 +569,10 @@ export interface PluginManifest {
|
|
|
290
569
|
hooks?: PluginHooks;
|
|
291
570
|
channels?: string[] | { events: string[]; actions: string[] };
|
|
292
571
|
capabilities?: Record<string, unknown>;
|
|
572
|
+
/** L3 DeviceState domains this plugin reads (props.deviceState), as Core passes
|
|
573
|
+
* them through from the bundle's declaration. Absent → fall back to the loaded
|
|
574
|
+
* bundle's own `stateDomains`. Read via resolvePluginRuntimeProps. */
|
|
575
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
293
576
|
_source?: string;
|
|
294
577
|
_connectorId?: string;
|
|
295
578
|
_mcpEndpoint?: string;
|
|
@@ -301,7 +584,13 @@ export interface RegisteredUIPlugin {
|
|
|
301
584
|
id: string;
|
|
302
585
|
type: 'ui' | 'hybrid';
|
|
303
586
|
capabilities?: NativeCapabilities;
|
|
587
|
+
/** L3 DeviceState domains this plugin's UI reads (props.deviceState). The
|
|
588
|
+
* trusted source for a HOST-registered plugin (no server manifest), read by
|
|
589
|
+
* resolvePluginRuntimeProps. Carried by register(), same as registerTools. */
|
|
590
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
304
591
|
Component: React.FC<PluginProps>;
|
|
592
|
+
/** Optional `tools/` section — a plugin may ship both UI and local tools. */
|
|
593
|
+
tools?: DeviceToolDefinition[];
|
|
305
594
|
}
|
|
306
595
|
|
|
307
596
|
export interface RegisteredServicePlugin {
|
|
@@ -313,4 +602,52 @@ export interface RegisteredServicePlugin {
|
|
|
313
602
|
onStop?: () => Promise<void> | void;
|
|
314
603
|
}
|
|
315
604
|
|
|
316
|
-
|
|
605
|
+
// ── Device tools (a plugin's `tools/` section) ──────────────
|
|
606
|
+
|
|
607
|
+
/** What a device tool implementation is handed at call time. */
|
|
608
|
+
export interface DeviceToolContext {
|
|
609
|
+
bridge: PluginBridge;
|
|
610
|
+
/** Device Bridge — camera, location, sensors, storage. */
|
|
611
|
+
native: PluginNative;
|
|
612
|
+
/**
|
|
613
|
+
* L3 semantic DeviceState (visual/spatial/activity/…), gated to the domains
|
|
614
|
+
* the plugin declared in `stateDomains`. Every domain is always present; an
|
|
615
|
+
* undeclared one answers with an explicit "not declared" rather than data —
|
|
616
|
+
* the same gating a UI plugin's `props.deviceState` gets. This is what lets an
|
|
617
|
+
* EXTERNAL tool (e.g. Ada Guide's `guide.observe`) read `ctx.deviceState.visual`
|
|
618
|
+
* over the public surface, with no SDK-internal access.
|
|
619
|
+
*/
|
|
620
|
+
deviceState: DeviceState;
|
|
621
|
+
/** Cooperative cancellation: caller cancelled, or the session went away. */
|
|
622
|
+
signal?: AbortSignal;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export type DeviceToolFn = (args: any, ctx: DeviceToolContext) => Promise<any> | any;
|
|
626
|
+
|
|
627
|
+
export interface DeviceToolDefinition {
|
|
628
|
+
/** Domain name, as the catalog and the caller see it. e.g. "guide.observe" */
|
|
629
|
+
name: string;
|
|
630
|
+
description?: string;
|
|
631
|
+
/** JSON Schema for arguments — advertised to Core, not enforced here. */
|
|
632
|
+
input_schema?: Record<string, unknown>;
|
|
633
|
+
run: DeviceToolFn;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** A plugin that ships tools and no UI at all. */
|
|
637
|
+
export interface RegisteredToolsPlugin {
|
|
638
|
+
id: string;
|
|
639
|
+
type: 'tools';
|
|
640
|
+
capabilities?: NativeCapabilities;
|
|
641
|
+
/**
|
|
642
|
+
* L3 DeviceState domains this plugin's tools may read via `ctx.deviceState`
|
|
643
|
+
* (e.g. `['visual']`). Undeclared domains answer "not declared". Same
|
|
644
|
+
* mechanism as a UI plugin's manifest `stateDomains`.
|
|
645
|
+
*/
|
|
646
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
647
|
+
tools: DeviceToolDefinition[];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export type RegisteredPlugin =
|
|
651
|
+
| RegisteredUIPlugin
|
|
652
|
+
| RegisteredServicePlugin
|
|
653
|
+
| RegisteredToolsPlugin;
|