@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
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,304 @@ 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
|
+
// ── Maps — where is X, what is here, how do I walk there ──
|
|
356
|
+
//
|
|
357
|
+
// One-shot verbs that THROW, like vision — not a state domain. Following a
|
|
358
|
+
// route over time (progress, off-route, "turn left in 20 m") is the reasoning
|
|
359
|
+
// loop's job over deviceState.spatial; this layer answers each question once.
|
|
360
|
+
//
|
|
361
|
+
// NOT ON-DEVICE — the opposite of vision, and the thing not to get wrong when
|
|
362
|
+
// describing a feature built on it. Every call sends the query and the
|
|
363
|
+
// coordinates you pass to a map service over the network: Apple's (MapKit /
|
|
364
|
+
// CLGeocoder) on iOS, the platform geocoder on Android. So the `maps` flag
|
|
365
|
+
// means exactly one thing: "this plugin may send place queries to Apple Maps".
|
|
366
|
+
//
|
|
367
|
+
// IT NEVER READS THE USER'S LOCATION. Every coordinate is the caller's — take a
|
|
368
|
+
// fix from `location.getCurrent` or `deviceState.spatial` and pass it in. That
|
|
369
|
+
// keeps `maps` from becoming a second, differently-gated door to where the
|
|
370
|
+
// user is.
|
|
371
|
+
//
|
|
372
|
+
// FOUND NOTHING IS AN ANSWER; COULD NOT LOOK THROWS. `[]` / `null` mean the
|
|
373
|
+
// service looked and there is nothing. Anything else THROWS, with a code:
|
|
374
|
+
// UNAVAILABLE this app build has no AdaMaps module (needs a native rebuild),
|
|
375
|
+
// or the platform has none — search/route are iOS only (MapKit)
|
|
376
|
+
// and refuse by name on Android. Also the service's own failures.
|
|
377
|
+
// OFFLINE the map service could not be reached.
|
|
378
|
+
// THROTTLED Apple is rate-limiting this app. A burst of queries is what
|
|
379
|
+
// trips it — search once per intent, not once per frame.
|
|
380
|
+
// And one refusal that is not a failure to look:
|
|
381
|
+
// INVALID_ARGUMENT the call itself is malformed — an empty query, a
|
|
382
|
+
// coordinate that is not { latitude, longitude } finite numbers in
|
|
383
|
+
// range (`{ lat, lng }` refuses), a limit or radius out of range.
|
|
384
|
+
// Nothing was sent; fix the call rather than retrying it.
|
|
385
|
+
|
|
386
|
+
/** The same `{ latitude, longitude }` `location.getCurrent` returns, so a fix
|
|
387
|
+
* passes straight in. */
|
|
388
|
+
export interface MapsCoordinate {
|
|
389
|
+
latitude: number;
|
|
390
|
+
longitude: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** A postal address, field by field. `null` = the map data has no value — never
|
|
394
|
+
* an empty string, which would read as a street with no name. */
|
|
395
|
+
export interface MapsAddress {
|
|
396
|
+
/** The placemark's own name — a building, or often number + street. */
|
|
397
|
+
name: string | null;
|
|
398
|
+
street: string | null;
|
|
399
|
+
streetNumber: string | null;
|
|
400
|
+
/** Neighbourhood / sub-locality. */
|
|
401
|
+
district: string | null;
|
|
402
|
+
city: string | null;
|
|
403
|
+
/** State / province / administrative area. */
|
|
404
|
+
region: string | null;
|
|
405
|
+
postalCode: string | null;
|
|
406
|
+
country: string | null;
|
|
407
|
+
/** ISO 3166-1 alpha-2, e.g. "GB". */
|
|
408
|
+
isoCountryCode: string | null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface MapsPlace {
|
|
412
|
+
/** What the map calls it ("Boots", "10 Downing St"). Null when unnamed. */
|
|
413
|
+
name: string | null;
|
|
414
|
+
coordinate: MapsCoordinate;
|
|
415
|
+
address: MapsAddress;
|
|
416
|
+
/** Apple's point-of-interest category as a vocabulary term ("Pharmacy",
|
|
417
|
+
* "Cafe", "PublicTransport") — NOT free text. Null for a plain address, or
|
|
418
|
+
* when the service gives none. */
|
|
419
|
+
category: string | null;
|
|
420
|
+
/** Straight-line metres from `opts.near`, when you passed one — NOT walking
|
|
421
|
+
* distance (ask `route` for that). Null without `near`. */
|
|
422
|
+
distanceM: number | null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export interface MapsRouteStep {
|
|
426
|
+
/** Apple's instruction ("Turn right onto Baker St"). The FIRST step is usually
|
|
427
|
+
* an origin step with an empty instruction — kept, not dropped, so step
|
|
428
|
+
* indexes line up with Apple's. */
|
|
429
|
+
instructions: string;
|
|
430
|
+
distanceM: number;
|
|
431
|
+
/** An advisory Apple attaches to this step. Null when there is none. */
|
|
432
|
+
notice: string | null;
|
|
433
|
+
polyline: MapsCoordinate[];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export interface MapsRoute {
|
|
437
|
+
mode: 'walking';
|
|
438
|
+
distanceM: number;
|
|
439
|
+
/** Apple's estimate for the whole route. */
|
|
440
|
+
expectedTravelTimeMs: number;
|
|
441
|
+
steps: MapsRouteStep[];
|
|
442
|
+
/** The whole route, start to end. */
|
|
443
|
+
polyline: MapsCoordinate[];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface MapsAPI {
|
|
447
|
+
/** Can this build search and route? True when the AdaMaps module is in the
|
|
448
|
+
* binary (iOS). False on Android and on an older build — `reverseGeocode`
|
|
449
|
+
* does not need the module and may still work there. */
|
|
450
|
+
isSupported(): Promise<boolean>;
|
|
451
|
+
/**
|
|
452
|
+
* Find places by free text — "pharmacy", "10 Downing Street".
|
|
453
|
+
*
|
|
454
|
+
* `near` centres the search on a coordinate YOU supply, within `radiusM`
|
|
455
|
+
* (default 3000, above 0 and at most 100 000). On iOS 18+ results are
|
|
456
|
+
* confined to that area; before 18 it is a strong hint and a farther result
|
|
457
|
+
* can come back — each result's `distanceM` says how far. At most `limit`
|
|
458
|
+
* results (default 8, from 1; more than 25 is capped at 25).
|
|
459
|
+
*
|
|
460
|
+
* Network, Apple's servers. `[]` = Apple looked and found nothing. iOS only:
|
|
461
|
+
* throws UNAVAILABLE on Android or a build without AdaMaps.
|
|
462
|
+
*/
|
|
463
|
+
search(query: string, opts?: { near?: MapsCoordinate; radiusM?: number; limit?: number }): Promise<MapsPlace[]>;
|
|
464
|
+
/**
|
|
465
|
+
* What is at a coordinate — its address and name.
|
|
466
|
+
*
|
|
467
|
+
* iOS AND Android (the platform geocoder); needs no AdaMaps module. The
|
|
468
|
+
* result's `coordinate` is the point you asked about, and its
|
|
469
|
+
* `category` / `distanceM` are null. `null` = nothing there.
|
|
470
|
+
*
|
|
471
|
+
* Network, not on-device. iOS reports a rate limit and a lost connection as
|
|
472
|
+
* the SAME error, so OFFLINE here can also mean "too many requests". Android's
|
|
473
|
+
* geocoder refuses without location permission (UNAVAILABLE) — that is the
|
|
474
|
+
* platform's rule, not a location read.
|
|
475
|
+
*/
|
|
476
|
+
reverseGeocode(at: MapsCoordinate): Promise<MapsPlace | null>;
|
|
477
|
+
/**
|
|
478
|
+
* A walking route between two coordinates YOU supply.
|
|
479
|
+
*
|
|
480
|
+
* `null` = Apple found no route — never a fake 0 m route, which would read as
|
|
481
|
+
* "you have arrived". Walking is the only mode; any other refuses by name.
|
|
482
|
+
* Network, Apple's servers. iOS only: throws UNAVAILABLE on Android or a build
|
|
483
|
+
* without AdaMaps.
|
|
484
|
+
*/
|
|
485
|
+
route(from: MapsCoordinate, to: MapsCoordinate, opts?: { mode?: 'walking' }): Promise<MapsRoute | null>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export interface BluetoothAPI {
|
|
489
|
+
/** Is BLE hardware present and permitted? */
|
|
490
|
+
isSupported(): Promise<boolean>;
|
|
491
|
+
/** Is the radio switched on? (Separate from permission — both can block.) */
|
|
492
|
+
isEnabled(): Promise<boolean>;
|
|
493
|
+
/**
|
|
494
|
+
* Scan for advertising devices for `timeoutMs`, then stop.
|
|
495
|
+
*
|
|
496
|
+
* Bounded rather than open-ended on purpose: a scan left running is a
|
|
497
|
+
* battery drain the caller cannot see, and the mistake is invisible until a
|
|
498
|
+
* user notices their phone is flat.
|
|
499
|
+
*/
|
|
500
|
+
scan(opts?: { timeoutMs?: number; serviceUUIDs?: string[] }): Promise<BluetoothDevice[]>;
|
|
501
|
+
}
|
|
502
|
+
|
|
174
503
|
export interface PluginNative {
|
|
175
504
|
camera: CameraAPI;
|
|
176
505
|
location: LocationAPI;
|
|
177
|
-
|
|
506
|
+
/**
|
|
507
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
508
|
+
* `storageFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
509
|
+
* single shared storage across plugins was a cross-plugin read/write hole;
|
|
510
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
511
|
+
* satisfy the type; new providers implement `storageFor`.
|
|
512
|
+
*/
|
|
513
|
+
storage?: StorageAPI;
|
|
178
514
|
/** @deprecated use `biometricsV2` for the native-backed API. This stub
|
|
179
515
|
* remains for backwards compatibility. */
|
|
180
516
|
biometrics: BiometricsAPI;
|
|
@@ -182,11 +518,44 @@ export interface PluginNative {
|
|
|
182
518
|
notifications: NotificationsAPI;
|
|
183
519
|
haptics: HapticsAPI;
|
|
184
520
|
connectivity: ConnectivityAPI;
|
|
185
|
-
|
|
521
|
+
/**
|
|
522
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
523
|
+
* `fileSystemFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
524
|
+
* single shared filesystem across plugins was a cross-plugin read/write hole;
|
|
525
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
526
|
+
* satisfy the type; new providers implement `fileSystemFor`.
|
|
527
|
+
*/
|
|
528
|
+
fileSystem?: FileSystemAPI;
|
|
529
|
+
/**
|
|
530
|
+
* Per-plugin filesystem: returns a FileSystemAPI whose sandbox root is scoped
|
|
531
|
+
* to THIS plugin (`plugin-files/<pluginId>/`), so one plugin cannot read,
|
|
532
|
+
* write or reach another plugin's files even via traversal. `gatePluginNative`
|
|
533
|
+
* uses it (passing the plugin's id) to isolate each plugin. If it is absent or
|
|
534
|
+
* no id is known, the gate FAILS CLOSED (blocks) rather than fall back to a
|
|
535
|
+
* shared filesystem — there is no shared bucket.
|
|
536
|
+
*/
|
|
537
|
+
fileSystemFor?(pluginId: string): FileSystemAPI;
|
|
538
|
+
/**
|
|
539
|
+
* Per-plugin storage: returns a StorageAPI whose KV namespace AND SQLite
|
|
540
|
+
* database are scoped to THIS plugin, so one plugin cannot read, overwrite,
|
|
541
|
+
* or query another plugin's key-value entries or tables. `gatePluginNative`
|
|
542
|
+
* uses it (passing the plugin's id) to isolate each plugin. Identity is
|
|
543
|
+
* threaded through the TRUSTED provider here — never a caller-supplied
|
|
544
|
+
* key/table prefix. If it is absent or no id is known, the gate FAILS CLOSED
|
|
545
|
+
* (blocks) rather than fall back to a shared store — there is no shared bucket.
|
|
546
|
+
*/
|
|
547
|
+
storageFor?(pluginId: string): StorageAPI;
|
|
186
548
|
cookies: CookiesAPI;
|
|
187
549
|
sharing: SharingAPI;
|
|
188
550
|
scanner: ScannerAPI;
|
|
189
551
|
nfc: NfcAPI;
|
|
552
|
+
/** SHAPE ONLY in this build — see BluetoothAPI. Present so it fails by name. */
|
|
553
|
+
bluetooth: BluetoothAPI;
|
|
554
|
+
/** SHAPE ONLY in this build — see VisionAPI. On-device image understanding. */
|
|
555
|
+
vision: VisionAPI;
|
|
556
|
+
/** Place search, reverse geocoding, walking routes — see MapsAPI. Sends
|
|
557
|
+
* queries to Apple's map servers; NOT on-device. */
|
|
558
|
+
maps: MapsAPI;
|
|
190
559
|
}
|
|
191
560
|
|
|
192
561
|
// ── Native capability flags (manifest declaration) ──────────
|
|
@@ -199,7 +568,20 @@ export interface NativeCapabilities {
|
|
|
199
568
|
backgroundTasks?: boolean;
|
|
200
569
|
fileSystem?: boolean;
|
|
201
570
|
notifications?: boolean;
|
|
571
|
+
/** BLE. Declaring it passes the gate but every call refuses by name until
|
|
572
|
+
* the native side exists — see BluetoothAPI. */
|
|
202
573
|
bluetooth?: boolean;
|
|
574
|
+
/** On-device image understanding (OCR, labels, frame quality). Refuses by
|
|
575
|
+
* name until the native module lands — see VisionAPI. */
|
|
576
|
+
vision?: boolean;
|
|
577
|
+
/** Place search, reverse geocoding, walking routes. Means "may send place
|
|
578
|
+
* queries to Apple Maps" — network, not on-device. See MapsAPI. */
|
|
579
|
+
maps?: boolean;
|
|
580
|
+
/** The live voice SESSION (props.voice). A privacy surface — a declaring
|
|
581
|
+
* plugin can read the conversation transcript — so it is gated like any
|
|
582
|
+
* other capability, and coupled to the platform's unsuppressable mic
|
|
583
|
+
* indicator. See VoiceAPI. */
|
|
584
|
+
voice?: boolean;
|
|
203
585
|
haptics?: boolean;
|
|
204
586
|
/** Read the platform cookie jar after a WebView login flow. */
|
|
205
587
|
cookies?: boolean;
|
|
@@ -258,7 +640,43 @@ export interface ThemeTokens {
|
|
|
258
640
|
|
|
259
641
|
export interface PluginProps {
|
|
260
642
|
bridge: PluginBridge;
|
|
261
|
-
|
|
643
|
+
/**
|
|
644
|
+
* L2 Device Primitives — the camera/location/storage/haptics/… surface, gated
|
|
645
|
+
* to the capabilities this plugin declared. This is the canonical name (R4);
|
|
646
|
+
* the underlying L1 native modules keep the `native` name, but what a plugin
|
|
647
|
+
* receives is `props.device`.
|
|
648
|
+
*/
|
|
649
|
+
device: PluginNative;
|
|
650
|
+
/**
|
|
651
|
+
* @deprecated Renamed to `device` in R4. Still passed (the SAME gated object)
|
|
652
|
+
* so existing/remote plugin bundles keep working — migrate to `props.device`.
|
|
653
|
+
*/
|
|
654
|
+
native?: PluginNative;
|
|
655
|
+
/**
|
|
656
|
+
* L3 DeviceState — the SEMANTIC layer (`props.deviceState.spatial/visual/…`),
|
|
657
|
+
* gated to the domains this plugin declared in `stateDomains`. Reads answer
|
|
658
|
+
* `unknown` rather than throwing; a domain the build has no engine for, or the
|
|
659
|
+
* plugin did not declare, returns an explicit refusal.
|
|
660
|
+
*
|
|
661
|
+
* REQUIRED, deliberately: an optional field is exactly the compile-time escape
|
|
662
|
+
* hatch that let a renderer ship without it (props.deviceState === undefined,
|
|
663
|
+
* which crashed on drawer/card). Every renderer builds this through
|
|
664
|
+
* resolvePluginRuntimeProps, so the constructor side always has it; making it
|
|
665
|
+
* required means a NEW renderer that forgets to pass it fails to compile.
|
|
666
|
+
*/
|
|
667
|
+
deviceState: DeviceState;
|
|
668
|
+
/**
|
|
669
|
+
* The live conversational SESSION, gated to plugins that declared
|
|
670
|
+
* `capabilities: { voice: true }`. A SESSION, not a device sensor: start/stop
|
|
671
|
+
* the loop, read + subscribe to the transcript, and speak first via
|
|
672
|
+
* `prompt()`. The mic-active indicator is platform-owned and cannot be
|
|
673
|
+
* suppressed from here.
|
|
674
|
+
*
|
|
675
|
+
* REQUIRED, like `deviceState`: every renderer builds it through
|
|
676
|
+
* resolvePluginRuntimeProps, so the object is always present. An undeclared
|
|
677
|
+
* plugin gets a refusal (reads say so, actions throw) — never a fake `idle`.
|
|
678
|
+
*/
|
|
679
|
+
voice: VoiceAPI;
|
|
262
680
|
theme: ThemeTokens;
|
|
263
681
|
context: PluginContext;
|
|
264
682
|
/** Engine-supplied props from `sys.focusUiPlugin(pluginId, props)`.
|
|
@@ -290,6 +708,11 @@ export interface PluginManifest {
|
|
|
290
708
|
hooks?: PluginHooks;
|
|
291
709
|
channels?: string[] | { events: string[]; actions: string[] };
|
|
292
710
|
capabilities?: Record<string, unknown>;
|
|
711
|
+
/** L3 DeviceState domains this plugin reads (props.deviceState), as Core passes
|
|
712
|
+
* them through from the solution's declaration. Absent → NO domains (fail
|
|
713
|
+
* closed): the loaded bundle's own `stateDomains` is never consulted when a
|
|
714
|
+
* manifest exists. Read via resolvePluginRuntimeProps. */
|
|
715
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
293
716
|
_source?: string;
|
|
294
717
|
_connectorId?: string;
|
|
295
718
|
_mcpEndpoint?: string;
|
|
@@ -301,7 +724,13 @@ export interface RegisteredUIPlugin {
|
|
|
301
724
|
id: string;
|
|
302
725
|
type: 'ui' | 'hybrid';
|
|
303
726
|
capabilities?: NativeCapabilities;
|
|
727
|
+
/** L3 DeviceState domains this plugin's UI reads (props.deviceState). The
|
|
728
|
+
* trusted source for a HOST-registered plugin (no server manifest), read by
|
|
729
|
+
* resolvePluginRuntimeProps. Carried by register(), same as registerTools. */
|
|
730
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
304
731
|
Component: React.FC<PluginProps>;
|
|
732
|
+
/** Optional `tools/` section — a plugin may ship both UI and local tools. */
|
|
733
|
+
tools?: DeviceToolDefinition[];
|
|
305
734
|
}
|
|
306
735
|
|
|
307
736
|
export interface RegisteredServicePlugin {
|
|
@@ -313,4 +742,56 @@ export interface RegisteredServicePlugin {
|
|
|
313
742
|
onStop?: () => Promise<void> | void;
|
|
314
743
|
}
|
|
315
744
|
|
|
316
|
-
|
|
745
|
+
// ── Device tools (a plugin's `tools/` section) ──────────────
|
|
746
|
+
|
|
747
|
+
/** What a device tool implementation is handed at call time. */
|
|
748
|
+
export interface DeviceToolContext {
|
|
749
|
+
bridge: PluginBridge;
|
|
750
|
+
/** Device Bridge — camera, location, sensors, storage. */
|
|
751
|
+
native: PluginNative;
|
|
752
|
+
/**
|
|
753
|
+
* L3 semantic DeviceState (visual/spatial/activity/…), gated to the domains
|
|
754
|
+
* the plugin declared in `stateDomains`. Every domain is always present; an
|
|
755
|
+
* undeclared one answers with an explicit "not declared" rather than data —
|
|
756
|
+
* the same gating a UI plugin's `props.deviceState` gets. This is what lets an
|
|
757
|
+
* EXTERNAL tool (e.g. Ada Guide's `guide.observe`) read `ctx.deviceState.visual`
|
|
758
|
+
* over the public surface, with no SDK-internal access.
|
|
759
|
+
*
|
|
760
|
+
* `subscribe` refuses (RESTRICTED) on every domain: a call has no lifetime to
|
|
761
|
+
* own a subscription, and the engines outlive it. Read instead.
|
|
762
|
+
*/
|
|
763
|
+
deviceState: DeviceState;
|
|
764
|
+
/** Cooperative cancellation: caller cancelled, or the session went away. */
|
|
765
|
+
signal?: AbortSignal;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export type DeviceToolFn = (args: any, ctx: DeviceToolContext) => Promise<any> | any;
|
|
769
|
+
|
|
770
|
+
export interface DeviceToolDefinition {
|
|
771
|
+
/** Domain name, as the catalog and the caller see it. e.g. "guide.observe" */
|
|
772
|
+
name: string;
|
|
773
|
+
description?: string;
|
|
774
|
+
/** JSON Schema for arguments — advertised to Core, not enforced here. */
|
|
775
|
+
input_schema?: Record<string, unknown>;
|
|
776
|
+
run: DeviceToolFn;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** A plugin that ships tools and no UI at all. */
|
|
780
|
+
export interface RegisteredToolsPlugin {
|
|
781
|
+
id: string;
|
|
782
|
+
type: 'tools';
|
|
783
|
+
capabilities?: NativeCapabilities;
|
|
784
|
+
/**
|
|
785
|
+
* L3 DeviceState domains this plugin's tools read via `ctx.deviceState` (e.g.
|
|
786
|
+
* `['visual']`). For a remote bundle this is the author's statement, not a
|
|
787
|
+
* grant: the tools get the manifest's `stateDomains` (resolvePluginGrants),
|
|
788
|
+
* and a domain declared here but not there is denied with a warning.
|
|
789
|
+
*/
|
|
790
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
791
|
+
tools: DeviceToolDefinition[];
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export type RegisteredPlugin =
|
|
795
|
+
| RegisteredUIPlugin
|
|
796
|
+
| RegisteredServicePlugin
|
|
797
|
+
| RegisteredToolsPlugin;
|