@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/dist/index.d.ts
CHANGED
|
@@ -5,12 +5,1185 @@ import * as react_native from 'react-native';
|
|
|
5
5
|
import { View, TextInput } from 'react-native';
|
|
6
6
|
export { ActivityIndicator, Animated, FlatList, Image, Modal, Platform, Pressable, RefreshControl, ScrollView, SectionList, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @adas/device-state — Device Bridge v2, the SEMANTIC layer.
|
|
10
|
+
*
|
|
11
|
+
* Intended repo path:
|
|
12
|
+
* packages/plugin-runtime/src/sdk/device-state.ts
|
|
13
|
+
* Intended module specifier for plugin bundles:
|
|
14
|
+
* '@adas/device-state' (NOT '@adas/plugin-sdk')
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* WHICH LAYER AM I IN?
|
|
18
|
+
*
|
|
19
|
+
* props.device.* MECHANICAL. "Do this to the hardware, now."
|
|
20
|
+
* VERB names: takePhoto, getCurrent, scanBarcode,
|
|
21
|
+
* readOnce, authenticate, pickImage.
|
|
22
|
+
* Returns a BARE value. THROWS when it cannot.
|
|
23
|
+
* Declared in the manifest as `capabilities: { camera: true }`
|
|
24
|
+
* (the legacy `native: {}` field is still honoured).
|
|
25
|
+
* Lives in '@adas/plugin-sdk'.
|
|
26
|
+
*
|
|
27
|
+
* props.deviceState.* SEMANTIC. "What is true right now, and how well do
|
|
28
|
+
* we know it?"
|
|
29
|
+
* STATE names: getSnapshot, subscribe, start, stop.
|
|
30
|
+
* Returns a SNAPSHOT carrying freshness + confidence,
|
|
31
|
+
* and able to say `unknown` in every field.
|
|
32
|
+
* A read NEVER throws for absence.
|
|
33
|
+
* Declared in the manifest as `stateDomains: ['visual']`.
|
|
34
|
+
* Lives in '@adas/device-state' (this file).
|
|
35
|
+
*
|
|
36
|
+
* The two containers are the discriminator. Every semantic call in a plugin
|
|
37
|
+
* begins with the eight characters `deviceState.`; every mechanical call
|
|
38
|
+
* begins with `native.`. There is no line of code where the layer is
|
|
39
|
+
* ambiguous, and no object on which both appear.
|
|
40
|
+
*
|
|
41
|
+
* This is a deliberate departure from
|
|
42
|
+
* Docs/device-bridge-v2/DEVICE_BRIDGE_V2_IMPLEMENTATION_GROUNDING_2026-08-07.md
|
|
43
|
+
* §"Migration strategy", which proposed adding `visual`, `spatial`,
|
|
44
|
+
* `connectivityState`… as fields ON `PluginNative`. See the design note at the
|
|
45
|
+
* bottom of this file for why that layout is rejected and what it costs.
|
|
46
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
47
|
+
*
|
|
48
|
+
* PROVENANCE MARKERS used throughout. They are not decoration — the freeze
|
|
49
|
+
* really does fix some of this and really does leave the rest open, and an
|
|
50
|
+
* implementor needs to know which is which before changing a line:
|
|
51
|
+
*
|
|
52
|
+
* FROZEN — API_FREEZE 2026-08-07 §15 lists it as a frozen candidate.
|
|
53
|
+
* V0.1 — "v0.1 freeze candidate; may change only when implementation
|
|
54
|
+
* grounding proves a field impossible/non-portable."
|
|
55
|
+
* OPEN — the docs explicitly leave it undecided. Do not quietly close it.
|
|
56
|
+
* DECISION — not in the docs at all; decided here, and reversible.
|
|
57
|
+
*/
|
|
58
|
+
/** FROZEN (API_FREEZE §1). The eight domain keys. */
|
|
59
|
+
type DeviceBridgeDomain = 'spatial' | 'visual' | 'activity' | 'deviceContext' | 'connectivity' | 'capability' | 'environment' | 'interaction';
|
|
60
|
+
type FreshnessState = 'fresh' | 'aging' | 'stale' | 'unknown';
|
|
61
|
+
/**
|
|
62
|
+
* FROZEN. Both fields are nullable on purpose: "we have no observation at all"
|
|
63
|
+
* has to be expressible without inventing a timestamp.
|
|
64
|
+
*
|
|
65
|
+
* `aging` is a real third state, not a rounding of fresh/stale — a consumer
|
|
66
|
+
* that only branches on fresh-vs-stale is misreading the contract.
|
|
67
|
+
*
|
|
68
|
+
* API_FREEZE §1 "Why observedAt + ageMs": the host may compute `ageMs` at read
|
|
69
|
+
* time, so it can be more accurate than `Date.now() - observedAt` measured on
|
|
70
|
+
* the JS side after a bridge hop. Read `ageMs` when you have it.
|
|
71
|
+
*/
|
|
72
|
+
interface Freshness {
|
|
73
|
+
state: FreshnessState;
|
|
74
|
+
/** Epoch ms of the observation itself. `null` = never observed. */
|
|
75
|
+
observedAt: number | null;
|
|
76
|
+
/** Age at read time. `null` = never observed. */
|
|
77
|
+
ageMs: number | null;
|
|
78
|
+
}
|
|
79
|
+
type ConfidenceLevel = 'high' | 'medium' | 'low' | 'unknown';
|
|
80
|
+
/**
|
|
81
|
+
* FROZEN. `level` is mandatory and ordinal; `score` is optional and exists
|
|
82
|
+
* only "when a stable numeric estimate exists" (API_FREEZE §1).
|
|
83
|
+
*
|
|
84
|
+
* The asymmetry is the point: native must not manufacture 0.5 to fill a field.
|
|
85
|
+
* A missing `score` is information — it says the estimator has no calibrated
|
|
86
|
+
* number, only a bucket.
|
|
87
|
+
*/
|
|
88
|
+
interface Confidence {
|
|
89
|
+
level: ConfidenceLevel;
|
|
90
|
+
/** 0..1. Omitted when no stable numeric estimate exists. */
|
|
91
|
+
score?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* FROZEN. Every snapshot in v2 carries exactly one of these.
|
|
95
|
+
*
|
|
96
|
+
* `freshness` required, `confidence` optional — because some domains report
|
|
97
|
+
* exact OS facts (battery level, audio route) where a confidence is
|
|
98
|
+
* meaningless, and others report inference (activity, indoor/outdoor) where it
|
|
99
|
+
* is mandatory. Cross-domain review: "Freshness is mandatory for dynamic
|
|
100
|
+
* state. Confidence is mandatory for inferred/estimated state; optional for
|
|
101
|
+
* exact OS facts."
|
|
102
|
+
*
|
|
103
|
+
* NOTE (OPEN): freshness is per-SNAPSHOT, never per-field. So a snapshot
|
|
104
|
+
* cannot today say "lighting is fresh but acoustics is stale". The
|
|
105
|
+
* architecture doc raised this for Spatial and Environment and never resolved
|
|
106
|
+
* it. Do not work around it by inventing a second freshness field.
|
|
107
|
+
*/
|
|
108
|
+
interface StateQuality {
|
|
109
|
+
freshness: Freshness;
|
|
110
|
+
confidence?: Confidence;
|
|
111
|
+
}
|
|
112
|
+
type SupportState = 'supported' | 'unsupported' | 'unknown';
|
|
113
|
+
type PermissionState$1 = 'granted' | 'denied' | 'restricted' | 'notDetermined' | 'limited' | 'unavailable' | 'unknown';
|
|
114
|
+
type AvailabilityState = 'available' | 'busy' | 'temporarilyUnavailable' | 'unavailable' | 'unknown';
|
|
115
|
+
type RestrictionState = 'none' | 'backgroundRestricted' | 'systemRestricted' | 'policyRestricted' | 'lowPowerRestricted' | 'unknown';
|
|
116
|
+
/**
|
|
117
|
+
* FROZEN (API_FREEZE core rule 4): "Events are meaningful semantic changes,
|
|
118
|
+
* never raw sensor streams." If you find yourself wanting an event per frame
|
|
119
|
+
* or per GPS fix, you are in the wrong layer.
|
|
120
|
+
*/
|
|
121
|
+
interface StateEvent<TName extends string = string, TPayload = unknown> {
|
|
122
|
+
domain: DeviceBridgeDomain;
|
|
123
|
+
name: TName;
|
|
124
|
+
/** Epoch ms. */
|
|
125
|
+
timestamp: number;
|
|
126
|
+
/**
|
|
127
|
+
* OPEN: per-event payload schemas are not specified anywhere in the three
|
|
128
|
+
* design docs. Defaults to `unknown` rather than `any` so a consumer is forced
|
|
129
|
+
* to narrow it rather than read a field that may not exist. A domain that
|
|
130
|
+
* decides its schemas (spatial — see `SpatialStateEvent`) passes `TPayload`,
|
|
131
|
+
* which is the freeze's own second parameter, dropped here until one did.
|
|
132
|
+
*/
|
|
133
|
+
payload?: TPayload;
|
|
134
|
+
}
|
|
135
|
+
interface Subscription {
|
|
136
|
+
unsubscribe(): void;
|
|
137
|
+
}
|
|
138
|
+
type StateListener<TName extends string = string> = (event: StateEvent<TName>) => void;
|
|
139
|
+
/**
|
|
140
|
+
* VOCABULARY §"Event Naming Rule", expressed as a type.
|
|
141
|
+
*
|
|
142
|
+
* Four domains (visual, activity, spatial, deviceContext) have NO frozen event
|
|
143
|
+
* vocabulary — the vocabulary doc publishes "Initial … Events" lists for
|
|
144
|
+
* connectivity, capability, interaction and environment only. Rather than mint
|
|
145
|
+
* names those docs deliberately did not mint, their events are typed against
|
|
146
|
+
* the naming RULE. A wrong-shaped name is a compile error; the specific
|
|
147
|
+
* vocabulary stays open, as the docs left it — except where a DECISION closes
|
|
148
|
+
* it (spatial, see `SpatialEvent`), and that list is checked against the rule
|
|
149
|
+
* through `AssertConforming`.
|
|
150
|
+
*/
|
|
151
|
+
type ConformingEventName = `${string}Changed` | `${string}Started` | `${string}Stopped` | `${string}Available` | `${string}Unavailable` | `${string}StateStale`;
|
|
152
|
+
/**
|
|
153
|
+
* A closed list of names, checked against the naming rule at compile time.
|
|
154
|
+
* Wrap a decided vocabulary in this and a name like `turnCompleted` stops
|
|
155
|
+
* compiling instead of reaching a plugin.
|
|
156
|
+
*/
|
|
157
|
+
type AssertConforming<T extends ConformingEventName> = T;
|
|
158
|
+
/** FROZEN (API_FREEZE §12). */
|
|
159
|
+
type DeviceBridgeErrorCode = 'UNSUPPORTED' | 'PERMISSION_DENIED' | 'RESTRICTED' | 'BUSY' | 'TEMPORARILY_UNAVAILABLE' | 'STALE' | 'TIMEOUT' | 'CANCELLED' | 'INTERNAL';
|
|
160
|
+
/**
|
|
161
|
+
* FROZEN shape (API_FREEZE §12).
|
|
162
|
+
*
|
|
163
|
+
* READ THE SCOPE LINE CAREFULLY. This is for FAILED OPERATIONS —
|
|
164
|
+
* `visual.capture()`, `visual.start()`. It is NOT how the surface reports that
|
|
165
|
+
* a camera is denied or a sensor is missing: "Expected state should still be
|
|
166
|
+
* exposed in snapshots."
|
|
167
|
+
*
|
|
168
|
+
* So: `getSnapshot()` does not throw this. `capture()` and `start()` do.
|
|
169
|
+
* That split is the whole no-fabrication contract in one sentence — a read
|
|
170
|
+
* always returns something honest, an action either happens or explains
|
|
171
|
+
* itself in a typed code.
|
|
172
|
+
*/
|
|
173
|
+
declare class DeviceBridgeError extends Error {
|
|
174
|
+
readonly code: DeviceBridgeErrorCode;
|
|
175
|
+
readonly domain?: DeviceBridgeDomain;
|
|
176
|
+
/** Whether retrying could plausibly succeed (BUSY yes, UNSUPPORTED no). */
|
|
177
|
+
readonly recoverable?: boolean;
|
|
178
|
+
constructor(code: DeviceBridgeErrorCode, message: string, opts?: {
|
|
179
|
+
domain?: DeviceBridgeDomain;
|
|
180
|
+
recoverable?: boolean;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
interface StateSession {
|
|
184
|
+
id: string;
|
|
185
|
+
/** The intent this session was started with. */
|
|
186
|
+
intent: string;
|
|
187
|
+
startedAt: number;
|
|
188
|
+
}
|
|
189
|
+
interface StartOptions<TIntent extends string> {
|
|
190
|
+
/**
|
|
191
|
+
* MANDATORY, and that is the single most important line in this file.
|
|
192
|
+
*
|
|
193
|
+
* You cannot open a v2 session without naming what it is FOR. Native then
|
|
194
|
+
* owns sampling rate, resolution, buffering, fusion and power — API_FREEZE
|
|
195
|
+
* core rule 6. `start()` means "maintain state according to this intent",
|
|
196
|
+
* not "open a particular native API" (§2).
|
|
197
|
+
*
|
|
198
|
+
* Contrast the mechanical layer, where `camera.takePhoto()` is a legal,
|
|
199
|
+
* complete, intent-free call. That difference is visible at every call site.
|
|
200
|
+
*/
|
|
201
|
+
intent: TIntent;
|
|
202
|
+
/**
|
|
203
|
+
* DECISION (not frozen). §13 requires bounded timeout/cancellation for
|
|
204
|
+
* session starts and says chain STOP must propagate to native, but leaves
|
|
205
|
+
* the plumbing "implementation-specific". `AbortSignal` is what
|
|
206
|
+
* `DeviceToolContext.signal` already carries in this SDK, so reuse it rather
|
|
207
|
+
* than invent a second cancellation idiom.
|
|
208
|
+
*/
|
|
209
|
+
signal?: AbortSignal;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* FROZEN pattern (API_FREEZE §2), reproduced as documentation. The eight
|
|
213
|
+
* concrete domains below do NOT extend this — each spells out exactly the
|
|
214
|
+
* methods it has, because §11 warns "domains should not be forced to implement
|
|
215
|
+
* meaningless lifecycle methods" and three domains genuinely have no intent
|
|
216
|
+
* vocabulary at all.
|
|
217
|
+
*
|
|
218
|
+
* Two invariants apply to every domain:
|
|
219
|
+
* - `getSnapshot()` reads CACHED state. It must not trigger expensive raw
|
|
220
|
+
* sensing (no camera open, no network probe, no GPS cold start).
|
|
221
|
+
* - "Multiple plugins may eventually share one native engine; session
|
|
222
|
+
* ownership must not imply duplicate sensor pipelines." (OPEN: the
|
|
223
|
+
* arbitration mechanism is undefined. See DESIGN NOTE 3.)
|
|
224
|
+
*/
|
|
225
|
+
interface StatefulDomain<TSnapshot, TIntent extends string = never, TEvent extends string = string> {
|
|
226
|
+
getSnapshot(): Promise<TSnapshot>;
|
|
227
|
+
start?(options: StartOptions<TIntent>): Promise<StateSession>;
|
|
228
|
+
stop?(sessionId?: string): Promise<void>;
|
|
229
|
+
subscribe?(listener: StateListener<TEvent>): Subscription;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Why every read is `Promise<T>` even though it is a cached read.
|
|
233
|
+
*
|
|
234
|
+
* Not because it samples — it must not. Because it crosses the RN bridge, and
|
|
235
|
+
* this SDK has no synchronous JSI module today (verified: everything in
|
|
236
|
+
* PluginNative except haptics/connectivity.isOnline is Promise-based). The
|
|
237
|
+
* grounding doc's own target for a warm cached snapshot is p50 < 10 ms /
|
|
238
|
+
* p95 < 30 ms, which is an async-bridge budget, not a sampling budget.
|
|
239
|
+
*/
|
|
240
|
+
/**
|
|
241
|
+
* FROZEN (API_FREEZE §3). Copied verbatim, not designed here.
|
|
242
|
+
*
|
|
243
|
+
* This landed as a placeholder first, on the belief that §3's field schema had
|
|
244
|
+
* never been written. It had — the placeholder was a gap in what got read, not
|
|
245
|
+
* a gap in the freeze. Worth recording, because "the docs leave this open" and
|
|
246
|
+
* "I did not find it" produce identical-looking comments and only one of them
|
|
247
|
+
* is a reason to invent a shape.
|
|
248
|
+
*/
|
|
249
|
+
type SpatialIntent = 'navigation' | 'fitness' | 'geofence' | 'lowPowerTracking' | 'preciseLocate';
|
|
250
|
+
/** FROZEN (API_FREEZE §3). */
|
|
251
|
+
interface SpatialPoint {
|
|
252
|
+
latitude: number;
|
|
253
|
+
longitude: number;
|
|
254
|
+
altitude?: number | null;
|
|
255
|
+
/** Metres. `null` when the platform gives no estimate — never 0, which
|
|
256
|
+
* would read as a perfect fix. */
|
|
257
|
+
accuracyM?: number | null;
|
|
258
|
+
timestamp: number;
|
|
259
|
+
}
|
|
260
|
+
/** FROZEN (API_FREEZE §3). */
|
|
261
|
+
interface SpatialSnapshot {
|
|
262
|
+
/** `null` when there is no usable fix. Not a zero coordinate — (0,0) is a
|
|
263
|
+
* real place in the Gulf of Guinea. */
|
|
264
|
+
position: SpatialPoint | null;
|
|
265
|
+
movement: {
|
|
266
|
+
speedMps: number | null;
|
|
267
|
+
bearingDeg: number | null;
|
|
268
|
+
distanceRecentM: number | null;
|
|
269
|
+
state?: 'stationary' | 'moving' | 'unknown';
|
|
270
|
+
};
|
|
271
|
+
orientation: {
|
|
272
|
+
headingDeg: number | null;
|
|
273
|
+
headingAccuracyDeg?: number | null;
|
|
274
|
+
pitchDeg?: number | null;
|
|
275
|
+
rollDeg?: number | null;
|
|
276
|
+
};
|
|
277
|
+
/** Newest last. Empty when nothing has been recorded — the ring is a native
|
|
278
|
+
* concern, and its window is chosen by intent. */
|
|
279
|
+
recentPath: SpatialPoint[];
|
|
280
|
+
changes?: {
|
|
281
|
+
lastMeaningfulTurnDeg?: number | null;
|
|
282
|
+
timestamp?: number | null;
|
|
283
|
+
};
|
|
284
|
+
quality: StateQuality;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* DECISION (not frozen) — the spatial event vocabulary. Six names, all ending
|
|
288
|
+
* in a suffix the naming rule allows, which `AssertConforming` enforces.
|
|
289
|
+
*
|
|
290
|
+
* Drawn from the architecture doc's spatial candidates (meaningful movement,
|
|
291
|
+
* meaningful turn, location quality degraded/recovered, state became stale)
|
|
292
|
+
* and named after the SNAPSHOT fields they report, so an event and a read use
|
|
293
|
+
* one vocabulary: `lastMeaningfulTurnChanged` is `changes.lastMeaningfulTurnDeg`
|
|
294
|
+
* changing. The names an earlier note floated (`turnCompleted`, `arrived`,
|
|
295
|
+
* `headingLost`) are rejected — the first two fail the rule, and "arrived" is a
|
|
296
|
+
* destination, which the vocabulary's Governance puts above Device Bridge.
|
|
297
|
+
*/
|
|
298
|
+
type SpatialEvent = AssertConforming<'movementStarted' | 'movementStopped' | 'lastMeaningfulTurnChanged' | 'positionConfidenceChanged' | 'positionChanged' | 'spatialStateStale'>;
|
|
299
|
+
/** A spatial event: the frozen envelope, with its payload always present. */
|
|
300
|
+
type SpatialEventOf<TName extends SpatialEvent, TPayload> = StateEvent<TName, TPayload> & {
|
|
301
|
+
domain: 'spatial';
|
|
302
|
+
payload: TPayload;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* DECISION — what each spatial event carries. Only the facts of the
|
|
306
|
+
* transition, in the snapshot's own field names; everything else is one
|
|
307
|
+
* `getSnapshot()` away, which keeps two shapes of the same fact from drifting.
|
|
308
|
+
*
|
|
309
|
+
* `timestamp` on the envelope is when the transition was RECOGNISED. Where a
|
|
310
|
+
* hold delays that on purpose, `since` carries the true onset.
|
|
311
|
+
*/
|
|
312
|
+
type SpatialStateEvent =
|
|
313
|
+
/** `movement.state` became `moving` and held (navigation: 3 s). */
|
|
314
|
+
SpatialEventOf<'movementStarted', {
|
|
315
|
+
previous: 'stationary' | 'unknown';
|
|
316
|
+
speedMps: number | null;
|
|
317
|
+
bearingDeg: number | null;
|
|
318
|
+
since: number;
|
|
319
|
+
}>
|
|
320
|
+
/** `moving` → `stationary`, held (navigation: 5 s). Never fired on moving →
|
|
321
|
+
* `unknown`: losing the speed is not evidence of stopping. */
|
|
322
|
+
| SpatialEventOf<'movementStopped', {
|
|
323
|
+
previous: 'moving';
|
|
324
|
+
since: number;
|
|
325
|
+
}>
|
|
326
|
+
/** A new completed turn. Signed: positive = clockwise = right. */
|
|
327
|
+
| SpatialEventOf<'lastMeaningfulTurnChanged', {
|
|
328
|
+
lastMeaningfulTurnDeg: number;
|
|
329
|
+
timestamp: number;
|
|
330
|
+
}>
|
|
331
|
+
/** The GPS fix band (high ≤10 m, medium ≤50 m, else low) changed and held.
|
|
332
|
+
* The fix alone — a compass calibration change is not a position event. */
|
|
333
|
+
| SpatialEventOf<'positionConfidenceChanged', {
|
|
334
|
+
level: ConfidenceLevel;
|
|
335
|
+
previous: ConfidenceLevel;
|
|
336
|
+
accuracyM: number | null;
|
|
337
|
+
direction: 'dropped' | 'recovered';
|
|
338
|
+
}>
|
|
339
|
+
/** Moved at least max(profile floor, 2 × accuracyM) from the last reported
|
|
340
|
+
* position, so GPS jitter never fires it. The session's first fix fires
|
|
341
|
+
* with `previous: null`. */
|
|
342
|
+
| SpatialEventOf<'positionChanged', {
|
|
343
|
+
position: SpatialPoint;
|
|
344
|
+
previous: SpatialPoint | null;
|
|
345
|
+
distanceM: number | null;
|
|
346
|
+
}>
|
|
347
|
+
/** `quality.freshness.state` became `stale`. Once; re-armed by a fresh read. */
|
|
348
|
+
| SpatialEventOf<'spatialStateStale', {
|
|
349
|
+
observedAt: number | null;
|
|
350
|
+
ageMs: number | null;
|
|
351
|
+
}>;
|
|
352
|
+
interface SpatialDomain {
|
|
353
|
+
getSnapshot(): Promise<SpatialSnapshot>;
|
|
354
|
+
start(options: StartOptions<SpatialIntent>): Promise<StateSession>;
|
|
355
|
+
stop(sessionId?: string): Promise<void>;
|
|
356
|
+
/**
|
|
357
|
+
* DECISION: the six `SpatialEvent`s, typed per name — narrow on `e.name` and
|
|
358
|
+
* `e.payload` is that event's shape.
|
|
359
|
+
*
|
|
360
|
+
* Events flow only while a spatial session is open; subscribing opens no
|
|
361
|
+
* sensor. They are derived from the same snapshot `getSnapshot()` returns: a
|
|
362
|
+
* listener that re-reads inside the callback sees the snapshot its event came
|
|
363
|
+
* from. A state change must hold, on at least two fixes, before it fires.
|
|
364
|
+
*
|
|
365
|
+
* From a device tool's `ctx.deviceState` this refuses (RESTRICTED): a call
|
|
366
|
+
* has no lifetime to own the listener.
|
|
367
|
+
*/
|
|
368
|
+
subscribe(listener: (event: SpatialStateEvent) => void): Subscription;
|
|
369
|
+
}
|
|
370
|
+
/** FROZEN (API_FREEZE §4) + VOCABULARY §"Visual Intents". Nine values. */
|
|
371
|
+
type VisualIntent = 'navigation' | 'document' | 'ocr' | 'shopping' | 'inspection' | 'accessibility' | 'stream' | 'highQuality' | 'lowPower';
|
|
372
|
+
/**
|
|
373
|
+
* Media crosses by REFERENCE, never as inline bytes (core rule 3).
|
|
374
|
+
*
|
|
375
|
+
* AN ID AND NOTHING ELSE. The `uri` field is gone (2026-08-16), and its
|
|
376
|
+
* removal answers the question this comment used to record as OPEN — "how does
|
|
377
|
+
* a plugin turn a handle into pixels?" It doesn't. Ever.
|
|
378
|
+
*
|
|
379
|
+
* Frames now live in a native ring keyed by id and are never written to disk.
|
|
380
|
+
* `native.vision.readText/describe/assessQuality` take this id and resolve it
|
|
381
|
+
* INSIDE the trusted layer; the pixels do not cross into plugin code, so a
|
|
382
|
+
* solution's bundle cannot copy, upload or retain them. That is what makes
|
|
383
|
+
* "frames never leave the phone" enforceable rather than a promise.
|
|
384
|
+
*
|
|
385
|
+
* Do not add `uri` or `bytes` back. A plugin that believes it needs the pixels
|
|
386
|
+
* needs a new method on `native.vision` instead — one whose implementation
|
|
387
|
+
* stays on this side of the boundary.
|
|
388
|
+
*/
|
|
389
|
+
interface MediaHandle {
|
|
390
|
+
id: string;
|
|
391
|
+
mimeType?: string;
|
|
392
|
+
/**
|
|
393
|
+
* Epoch ms after which this id no longer resolves — the frame's real
|
|
394
|
+
* eviction time from the native ring, not a guess.
|
|
395
|
+
*
|
|
396
|
+
* Past it, `native.vision` throws rather than returning an empty result:
|
|
397
|
+
* "I cannot look at that frame" and "I looked and found nothing" are
|
|
398
|
+
* different answers and must never be confused by a caller about to speak.
|
|
399
|
+
* In practice a caller rarely hits this, because the timeline is clamped to
|
|
400
|
+
* native retention — see `visualEngine.effectiveWindowMs()`.
|
|
401
|
+
*/
|
|
402
|
+
expiresAt?: number | null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* V0.1. Per-frame usability is CATEGORICAL, not numeric — 'degraded' rather
|
|
406
|
+
* than a blur score. Coarse honest classes beat false precision; the same
|
|
407
|
+
* principle the connectivity doc states outright for connection quality.
|
|
408
|
+
*
|
|
409
|
+
* Every member carries `'unknown'` so a platform that cannot assess blur says
|
|
410
|
+
* so instead of defaulting to 'good'.
|
|
411
|
+
*/
|
|
412
|
+
interface VisualFrameQuality {
|
|
413
|
+
blur?: 'good' | 'degraded' | 'unknown';
|
|
414
|
+
exposure?: 'good' | 'dark' | 'bright' | 'unknown';
|
|
415
|
+
/** `null` = cannot tell. Distinct from `false` = checked, not obstructed. */
|
|
416
|
+
obstructed?: boolean | null;
|
|
417
|
+
}
|
|
418
|
+
interface VisualFrameRef {
|
|
419
|
+
handle: MediaHandle;
|
|
420
|
+
timestamp: number;
|
|
421
|
+
orientation?: {
|
|
422
|
+
headingDeg?: number;
|
|
423
|
+
pitchDeg?: number;
|
|
424
|
+
rollDeg?: number;
|
|
425
|
+
};
|
|
426
|
+
quality?: VisualFrameQuality;
|
|
427
|
+
}
|
|
428
|
+
interface VisualSnapshot {
|
|
429
|
+
camera: {
|
|
430
|
+
available: boolean;
|
|
431
|
+
/**
|
|
432
|
+
* Independent of `available` on purpose. A camera can be present and
|
|
433
|
+
* permitted (`available: true`) while no session is running
|
|
434
|
+
* (`active: false`) — that is a different situation from a camera the app
|
|
435
|
+
* cannot use at all, and Guide has to say different things about them.
|
|
436
|
+
*
|
|
437
|
+
* DRIFT (flagged, not resolved): the architecture doc's conceptual block
|
|
438
|
+
* also carried `camera.orientation`; the frozen §4 block does not. Not
|
|
439
|
+
* added here.
|
|
440
|
+
*/
|
|
441
|
+
active: boolean;
|
|
442
|
+
facing?: 'front' | 'back' | 'unknown';
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* `null` when there is no usable current frame.
|
|
446
|
+
*
|
|
447
|
+
* This is the anti-fabrication field. The alternative implementation —
|
|
448
|
+
* returning the last frame you happen to still hold — is precisely how a
|
|
449
|
+
* guidance loop ends up describing a corridor the user left thirty seconds
|
|
450
|
+
* ago. If it is not current, it is `null`, and the timeline below is where
|
|
451
|
+
* you go looking for history, knowingly.
|
|
452
|
+
*/
|
|
453
|
+
latest: VisualFrameRef | null;
|
|
454
|
+
/**
|
|
455
|
+
* TIME-based, not frame-count-based. Default window ~5s; documented bounded
|
|
456
|
+
* range roughly 1–15s, selected by profile/intent. Native decides how
|
|
457
|
+
* densely to retain frames inside the window and evicts everything outside
|
|
458
|
+
* it automatically.
|
|
459
|
+
*
|
|
460
|
+
* `windowMs` is exposed; FPS knobs are NOT, and must never be added
|
|
461
|
+
* (API_FREEZE §4). If a plugin wants "more frames", it declares a different
|
|
462
|
+
* intent — it does not turn a dial.
|
|
463
|
+
*/
|
|
464
|
+
recentTimeline: {
|
|
465
|
+
windowMs: number;
|
|
466
|
+
frames: VisualFrameRef[];
|
|
467
|
+
};
|
|
468
|
+
quality: StateQuality;
|
|
469
|
+
}
|
|
470
|
+
interface VisualCaptureResult {
|
|
471
|
+
frame: VisualFrameRef;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* OPEN, worth knowing before you use it: whether `capture({intent})` may pass
|
|
475
|
+
* an intent DIFFERENT from the running session's intent is not addressed
|
|
476
|
+
* anywhere in the three docs.
|
|
477
|
+
*/
|
|
478
|
+
interface VisualCaptureOptions {
|
|
479
|
+
intent: VisualIntent;
|
|
480
|
+
/** §13: capture must support bounded timeout/cancellation. */
|
|
481
|
+
signal?: AbortSignal;
|
|
482
|
+
timeoutMs?: number;
|
|
483
|
+
}
|
|
484
|
+
interface VisualDomain {
|
|
485
|
+
/**
|
|
486
|
+
* Cached read. Does NOT open the camera and does NOT take a picture.
|
|
487
|
+
*
|
|
488
|
+
* This is the call a guidance loop makes on every turn. The hot path is:
|
|
489
|
+
* native camera pipeline → rolling native timeline → cached visual state →
|
|
490
|
+
* one snapshot read from RN. "Avoid repeatedly starting the camera or
|
|
491
|
+
* capturing a new image for every plugin-tool call during an interactive
|
|
492
|
+
* session."
|
|
493
|
+
*/
|
|
494
|
+
getSnapshot(): Promise<VisualSnapshot>;
|
|
495
|
+
/** Maintain visual state according to this intent. Not "open the camera". */
|
|
496
|
+
start(options: StartOptions<VisualIntent>): Promise<StateSession>;
|
|
497
|
+
/**
|
|
498
|
+
* An intentional, one-shot, high-quality-ish acquisition. THROWS
|
|
499
|
+
* DeviceBridgeError on failure — it is an operation, not a read.
|
|
500
|
+
*
|
|
501
|
+
* If you are about to call this once per conversational turn, stop: that is
|
|
502
|
+
* `getSnapshot()`'s job, and doing it here is the frame-timer pattern this
|
|
503
|
+
* whole layer exists to replace.
|
|
504
|
+
*/
|
|
505
|
+
capture(options: VisualCaptureOptions): Promise<VisualCaptureResult>;
|
|
506
|
+
/**
|
|
507
|
+
* OPEN (doc conflict): the architecture doc shows `visual.subscribe(events)`
|
|
508
|
+
* — a filter — while API_FREEZE shows `visual.subscribe(listener)`. The
|
|
509
|
+
* freeze is followed here. Unresolved.
|
|
510
|
+
*
|
|
511
|
+
* OPEN: there is no "Initial Visual Events" list anywhere. The candidate set
|
|
512
|
+
* in prose (camera became available/unavailable, frame quality degraded/
|
|
513
|
+
* recovered, camera appears obstructed, significant scene change, visual
|
|
514
|
+
* session suspended/resumed, visual state became stale) is NOT a minted
|
|
515
|
+
* vocabulary. Typed against the naming rule only.
|
|
516
|
+
*/
|
|
517
|
+
subscribe(listener: StateListener<ConformingEventName>): Subscription;
|
|
518
|
+
stop(sessionId?: string): Promise<void>;
|
|
519
|
+
}
|
|
520
|
+
/** FROZEN (API_FREEZE §5, VOCABULARY §"Human Activity Values"). */
|
|
521
|
+
type HumanActivity = 'stationary' | 'walking' | 'running' | 'cycling' | 'driving' | 'unknown';
|
|
522
|
+
/** FROZEN. Independent of HumanActivity — walking+pocket, driving+mounted. */
|
|
523
|
+
type DevicePlacement = 'handheld' | 'pocket' | 'table' | 'mounted' | 'unknown';
|
|
524
|
+
/**
|
|
525
|
+
* FROZEN. Note `driving` is BOTH an intent here and a HumanActivity value
|
|
526
|
+
* above. The docs define no relationship between the two and do not flag the
|
|
527
|
+
* collision: declaring intent `driving` does not assert the user is driving.
|
|
528
|
+
*/
|
|
529
|
+
type ActivityIntent = 'navigation' | 'fitness' | 'driving' | 'safety' | 'accessibility' | 'lowPower';
|
|
530
|
+
interface ActivitySnapshot {
|
|
531
|
+
human: {
|
|
532
|
+
state: HumanActivity;
|
|
533
|
+
/** `null` when the start of the current activity is not known. */
|
|
534
|
+
since: number | null;
|
|
535
|
+
/** REQUIRED, unlike StateQuality.confidence — an inferred activity may
|
|
536
|
+
* never be handed over without saying how sure the inference is. */
|
|
537
|
+
confidence: Confidence;
|
|
538
|
+
};
|
|
539
|
+
devicePose: {
|
|
540
|
+
placement: DevicePlacement;
|
|
541
|
+
/** V0.1. These four values appear only in the freeze TS, not in the
|
|
542
|
+
* vocabulary doc — unlike the placement values. Mild drift, flagged. */
|
|
543
|
+
orientation?: 'portrait' | 'landscape' | 'flat' | 'unknown';
|
|
544
|
+
confidence: Confidence;
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* Advisory only. `likelyNext` is a hint and never a fact.
|
|
548
|
+
* OPEN: transition names are bare `string` — no controlled vocabulary exists
|
|
549
|
+
* anywhere, which sits awkwardly beside "Intents are not arbitrary strings".
|
|
550
|
+
*/
|
|
551
|
+
transition?: {
|
|
552
|
+
current?: string | null;
|
|
553
|
+
likelyNext?: string | null;
|
|
554
|
+
confidence?: Confidence;
|
|
555
|
+
};
|
|
556
|
+
/** OPEN: no window length or cap is specified for Activity (Spatial gets
|
|
557
|
+
* ~10–20s, Visual ~5s). Required array — an empty one is a real answer. */
|
|
558
|
+
recentTransitions: Array<{
|
|
559
|
+
name: string;
|
|
560
|
+
timestamp: number;
|
|
561
|
+
}>;
|
|
562
|
+
quality: StateQuality;
|
|
563
|
+
}
|
|
564
|
+
interface ActivityDomain {
|
|
565
|
+
getSnapshot(): Promise<ActivitySnapshot>;
|
|
566
|
+
/**
|
|
567
|
+
* OPEN: §11's naming list shows only `activity.getSnapshot()`, and §2 makes
|
|
568
|
+
* start/stop optional. Whether Activity is session-based or a passive
|
|
569
|
+
* always-on domain is genuinely unstated. Included here because the six
|
|
570
|
+
* frozen ActivityIntent values would otherwise have no seam to arrive
|
|
571
|
+
* through — but this is a DECISION, not a reading of the freeze.
|
|
572
|
+
*/
|
|
573
|
+
start(options: StartOptions<ActivityIntent>): Promise<StateSession>;
|
|
574
|
+
stop(sessionId?: string): Promise<void>;
|
|
575
|
+
/** OPEN: no "Initial Activity Events" list exists. */
|
|
576
|
+
subscribe(listener: StateListener<ConformingEventName>): Subscription;
|
|
577
|
+
}
|
|
578
|
+
interface DeviceContextSnapshot {
|
|
579
|
+
power: {
|
|
580
|
+
/** 0..1 FRACTION, not a percentage. `null` = not known. */
|
|
581
|
+
batteryLevel: number | null;
|
|
582
|
+
charging: boolean | null;
|
|
583
|
+
lowPowerMode: boolean | null;
|
|
584
|
+
};
|
|
585
|
+
thermal: {
|
|
586
|
+
state: 'nominal' | 'fair' | 'serious' | 'critical' | 'unknown';
|
|
587
|
+
};
|
|
588
|
+
resources: {
|
|
589
|
+
memoryPressure?: 'normal' | 'warning' | 'critical' | 'unknown';
|
|
590
|
+
storagePressure?: 'normal' | 'warning' | 'critical' | 'unknown';
|
|
591
|
+
};
|
|
592
|
+
/** Confidence is legitimately omitted here — these are exact OS facts, not
|
|
593
|
+
* estimates. Freshness still matters: a populated batteryLevel says nothing
|
|
594
|
+
* about whether it was sampled a second or a minute ago. */
|
|
595
|
+
quality: StateQuality;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* NO INTENT TYPE, and that is deliberate — not an oversight, and not a gap to
|
|
599
|
+
* fill in later. There is no `DeviceContextIntent` in the freeze and no
|
|
600
|
+
* "Device Context Intents" section in the vocabulary, because
|
|
601
|
+
* (cross-domain review) "A domain may expose an intent only if it changes
|
|
602
|
+
* native implementation strategy in a meaningful way." Reading battery and
|
|
603
|
+
* thermal does not.
|
|
604
|
+
*
|
|
605
|
+
* The direction of flow is the other way round: Device Context is an INPUT to
|
|
606
|
+
* how OTHER domains satisfy their intents. Visual intent `navigation` +
|
|
607
|
+
* device context `battery 5% + low-power mode` → native picks a lower-power
|
|
608
|
+
* camera strategy while still honouring the Visual contract. The plugin does
|
|
609
|
+
* not retune anything, and must not be asked to.
|
|
610
|
+
*
|
|
611
|
+
* Do not add `deviceContext.start({intent:'lowPower'})` because five other
|
|
612
|
+
* domains have a lowPower intent.
|
|
613
|
+
*/
|
|
614
|
+
interface DeviceContextDomain {
|
|
615
|
+
getSnapshot(): Promise<DeviceContextSnapshot>;
|
|
616
|
+
/**
|
|
617
|
+
* OPEN: `DeviceContextDomain`'s interface body is never written in the
|
|
618
|
+
* freeze, and no Device Context event names are frozen (the vocabulary has
|
|
619
|
+
* "Initial … Events" for connectivity/capability/interaction/environment
|
|
620
|
+
* only). DECISION: expose subscribe, because the candidate events —
|
|
621
|
+
* low-power mode changed, charging changed, thermal pressure changed — are
|
|
622
|
+
* exactly what an adaptive session needs and polling for them is wasteful.
|
|
623
|
+
*
|
|
624
|
+
* OPEN: "battery crossed significant threshold" and "changed materially"
|
|
625
|
+
* have no numbers anywhere, and Device Context has no profiles to
|
|
626
|
+
* parameterize them.
|
|
627
|
+
*/
|
|
628
|
+
subscribe(listener: StateListener<ConformingEventName>): Subscription;
|
|
629
|
+
}
|
|
630
|
+
/** FROZEN (API_FREEZE §7, VOCABULARY §"Connectivity Intents"). */
|
|
631
|
+
type ConnectivityIntent = 'lowLatencyConversation' | 'interactive' | 'bulkTransfer' | 'backgroundSync' | 'offlineResilient' | 'lowPower';
|
|
632
|
+
/**
|
|
633
|
+
* V0.1, and note it is FLAT — the connectivity design doc sketched a nested
|
|
634
|
+
* shape (internet{…}, quality{…}, recent_history{…}) and the freeze flattened
|
|
635
|
+
* it. Two unreconciled deltas, carried here as-is rather than merged:
|
|
636
|
+
* - `bandwidthClass` appears in the freeze but in neither the design doc nor
|
|
637
|
+
* the vocabulary's Connectivity Values.
|
|
638
|
+
* - there is NO recent-history field, although the design doc requires native
|
|
639
|
+
* to maintain recent transitions and leaves the default window OPEN.
|
|
640
|
+
*/
|
|
641
|
+
interface ConnectivitySnapshot {
|
|
642
|
+
/** NOTE: the design doc says "online / offline / uncertain"; the frozen
|
|
643
|
+
* third value is `unknown`. The two words are never reconciled. */
|
|
644
|
+
reachability: 'online' | 'offline' | 'unknown';
|
|
645
|
+
transport: 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none' | 'unknown';
|
|
646
|
+
qualityClass: 'excellent' | 'good' | 'degraded' | 'poor' | 'unusable' | 'unknown';
|
|
647
|
+
stability: 'stable' | 'changing' | 'flapping' | 'unknown';
|
|
648
|
+
/** `null` when no recent measurement exists. Explicitly preferred over
|
|
649
|
+
* reporting a stale number: "return unknown rather than stale precision." */
|
|
650
|
+
latencyMs?: number | null;
|
|
651
|
+
bandwidthClass?: 'high' | 'medium' | 'low' | 'unknown';
|
|
652
|
+
metered?: boolean | null;
|
|
653
|
+
constrained?: boolean | null;
|
|
654
|
+
quality: StateQuality;
|
|
655
|
+
}
|
|
656
|
+
/** FROZEN v0.1 (VOCABULARY §"Initial Connectivity Events"). All ten. */
|
|
657
|
+
type ConnectivityEvent = 'connectivityOnline' | 'connectivityOffline' | 'transportChanged' | 'connectivityDegraded' | 'connectivityRecovered' | 'connectivityUnstable' | 'connectivityStable' | 'meteredStateChanged' | 'constrainedStateChanged' | 'connectivityStateStale';
|
|
658
|
+
interface ConnectivityDomain {
|
|
659
|
+
/**
|
|
660
|
+
* A fast local cached read. It must NOT ping, and must NOT run a bandwidth
|
|
661
|
+
* or speed test. Latency is sampled on native's own schedule, chosen by
|
|
662
|
+
* intent, and cached with a timestamp.
|
|
663
|
+
*/
|
|
664
|
+
getSnapshot(): Promise<ConnectivitySnapshot>;
|
|
665
|
+
/**
|
|
666
|
+
* OPEN: `ConnectivityDomain` is named in §11 but never defined, so whether
|
|
667
|
+
* connectivity is session-based is unstated. DECISION: yes — six frozen
|
|
668
|
+
* intents with documented behavioural differences (lowLatencyConversation
|
|
669
|
+
* "prioritize fresh latency knowledge"; lowPower "rely primarily on passive
|
|
670
|
+
* OS signals and minimize active probes") only mean something if there is a
|
|
671
|
+
* way to declare one.
|
|
672
|
+
*/
|
|
673
|
+
start(options: StartOptions<ConnectivityIntent>): Promise<StateSession>;
|
|
674
|
+
stop(sessionId?: string): Promise<void>;
|
|
675
|
+
subscribe(listener: StateListener<ConnectivityEvent>): Subscription;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* FROZEN. Four independent axes, deliberately not collapsed. The canonical
|
|
679
|
+
* worked example from the capability doc: supported + granted + busy + none —
|
|
680
|
+
* a camera you own, are allowed to use, and cannot have right now.
|
|
681
|
+
*/
|
|
682
|
+
interface CapabilityStatus {
|
|
683
|
+
support: SupportState;
|
|
684
|
+
permission?: PermissionState$1;
|
|
685
|
+
availability: AvailabilityState;
|
|
686
|
+
restriction?: RestrictionState;
|
|
687
|
+
}
|
|
688
|
+
interface CapabilitySnapshot {
|
|
689
|
+
camera: CapabilityStatus;
|
|
690
|
+
microphone: CapabilityStatus;
|
|
691
|
+
/** The only capability the freeze gives scoped detail to. Health and
|
|
692
|
+
* photos/media do NOT get one — see the gap note below. */
|
|
693
|
+
location: CapabilityStatus & {
|
|
694
|
+
precision?: 'precise' | 'approximate' | 'unknown';
|
|
695
|
+
backgroundPermission?: PermissionState$1;
|
|
696
|
+
};
|
|
697
|
+
motion: CapabilityStatus;
|
|
698
|
+
bluetooth: CapabilityStatus;
|
|
699
|
+
uwb: CapabilityStatus;
|
|
700
|
+
nfc: CapabilityStatus;
|
|
701
|
+
notifications: CapabilityStatus;
|
|
702
|
+
/**
|
|
703
|
+
* GAP (flagged): the capability doc notes health "authorization is often
|
|
704
|
+
* record-type specific rather than a single global permission" and its
|
|
705
|
+
* candidate shape had a `permission_summary`. The frozen §9 gives health a
|
|
706
|
+
* plain CapabilityStatus, so scoped health authorization is NOT
|
|
707
|
+
* representable in v0.1. Its own exit criterion ("scoped permissions are
|
|
708
|
+
* representable") is therefore only partly met.
|
|
709
|
+
*/
|
|
710
|
+
health: CapabilityStatus;
|
|
711
|
+
backgroundExecution: CapabilityStatus;
|
|
712
|
+
/** GAP (flagged): photos/media appears in the design doc's native inputs and
|
|
713
|
+
* its full-vs-limited scope discussion, but there is NO photos key in the
|
|
714
|
+
* frozen CapabilitySnapshot. Not added here. */
|
|
715
|
+
quality: StateQuality;
|
|
716
|
+
}
|
|
717
|
+
/** FROZEN v0.1 (VOCABULARY §"Initial Capability Events"). All ten. */
|
|
718
|
+
type CapabilityEvent = 'capabilityChanged' | 'permissionChanged' | 'capabilityAvailable' | 'capabilityUnavailable'
|
|
719
|
+
/** NOTE: this pair does not match the documented naming patterns. The docs
|
|
720
|
+
* neither flag nor reconcile it; carried verbatim rather than "fixed". */
|
|
721
|
+
| 'capabilityBusy' | 'capabilityFree' | 'restrictionChanged' | 'locationPrecisionChanged' | 'backgroundPermissionChanged' | 'capabilityStateStale';
|
|
722
|
+
/**
|
|
723
|
+
* NO INTENT TYPE — the one domain that is intent-less by explicit reasoning
|
|
724
|
+
* rather than by omission: "Capability State itself should generally be cheap
|
|
725
|
+
* and mostly event-driven, so it does not need many sensing profiles."
|
|
726
|
+
* A candidate `interactive/background/minimal` vocabulary is floated in the
|
|
727
|
+
* design doc and explicitly conditional ("only be kept if implementation
|
|
728
|
+
* proves they add value"). Nothing frozen. Not added.
|
|
729
|
+
*
|
|
730
|
+
* Also NOT here: any `requestPermission()`. Permission-request UX is outside
|
|
731
|
+
* this state engine by design, and a floated generic primitive is future work.
|
|
732
|
+
*/
|
|
733
|
+
interface CapabilityDomain {
|
|
734
|
+
getSnapshot(): Promise<CapabilitySnapshot>;
|
|
735
|
+
subscribe(listener: StateListener<CapabilityEvent>): Subscription;
|
|
736
|
+
}
|
|
737
|
+
/** FROZEN (VOCABULARY §"Environment Intents"). */
|
|
738
|
+
type EnvironmentIntent = 'ambient' | 'navigation' | 'accessibility' | 'proximity' | 'safety' | 'lowPower';
|
|
739
|
+
interface EnvironmentSnapshot {
|
|
740
|
+
/** Privacy is part of the SHAPE, not advice: this says `dim`, never a camera
|
|
741
|
+
* frame; `loud`, never microphone audio; a beacon count, never a BLE
|
|
742
|
+
* payload. Raw audio/images/BLE payloads are not part of this domain. */
|
|
743
|
+
lighting: {
|
|
744
|
+
level: 'dark' | 'dim' | 'normal' | 'bright' | 'veryBright' | 'unknown';
|
|
745
|
+
lux?: number | null;
|
|
746
|
+
confidence?: Confidence;
|
|
747
|
+
};
|
|
748
|
+
acoustics: {
|
|
749
|
+
level: 'quiet' | 'moderate' | 'loud' | 'veryLoud' | 'unknown';
|
|
750
|
+
approximateDb?: number | null;
|
|
751
|
+
confidence?: Confidence;
|
|
752
|
+
};
|
|
753
|
+
/** `uncertain` and `unknown` are different: uncertain = we looked and cannot
|
|
754
|
+
* decide; unknown = we have no basis to decide. */
|
|
755
|
+
enclosure: {
|
|
756
|
+
state: 'indoor' | 'outdoor' | 'uncertain' | 'unknown';
|
|
757
|
+
confidence?: Confidence;
|
|
758
|
+
};
|
|
759
|
+
proximity?: {
|
|
760
|
+
distanceClass?: 'immediate' | 'near' | 'far' | 'unknown';
|
|
761
|
+
confidence?: Confidence;
|
|
762
|
+
};
|
|
763
|
+
/** The only sub-block with no confidence — these are presence booleans, not
|
|
764
|
+
* estimates. Note they cannot distinguish unsupported / denied / switched
|
|
765
|
+
* off; that distinction lives in `capability`. */
|
|
766
|
+
nearbySignals?: {
|
|
767
|
+
bleAvailable?: boolean;
|
|
768
|
+
beaconCount?: number | null;
|
|
769
|
+
uwbAvailable?: boolean;
|
|
770
|
+
nfcAvailable?: boolean;
|
|
771
|
+
};
|
|
772
|
+
quality: StateQuality;
|
|
773
|
+
}
|
|
774
|
+
/** FROZEN v0.1 (VOCABULARY §"Initial Environment Events"). All ten. */
|
|
775
|
+
type EnvironmentEvent = 'lightingChanged' | 'environmentBecameDark' | 'environmentBecameBright' | 'noiseLevelChanged' | 'environmentBecameLoud' | 'indoorOutdoorChanged' | 'proximityChanged' | 'nearbySignalAppeared' | 'nearbySignalDisappeared' | 'environmentStateStale';
|
|
776
|
+
interface EnvironmentDomain {
|
|
777
|
+
getSnapshot(): Promise<EnvironmentSnapshot>;
|
|
778
|
+
start(options: StartOptions<EnvironmentIntent>): Promise<StateSession>;
|
|
779
|
+
stop(sessionId?: string): Promise<void>;
|
|
780
|
+
subscribe(listener: StateListener<EnvironmentEvent>): Subscription;
|
|
781
|
+
}
|
|
782
|
+
interface InteractionSnapshot {
|
|
783
|
+
lifecycle: {
|
|
784
|
+
appState: 'foreground' | 'background' | 'inactive' | 'unknown';
|
|
785
|
+
screenOn: boolean | null;
|
|
786
|
+
userActive?: boolean | null;
|
|
787
|
+
idleDurationMs?: number | null;
|
|
788
|
+
};
|
|
789
|
+
audio: {
|
|
790
|
+
captureActive: boolean | null;
|
|
791
|
+
playbackActive: boolean | null;
|
|
792
|
+
route: 'speaker' | 'earpiece' | 'wiredHeadset' | 'bluetooth' | 'carAudio' | 'other' | 'unknown';
|
|
793
|
+
/** `none` (checked, nothing interrupting) and `unknown` (no knowledge) are
|
|
794
|
+
* deliberately different values. */
|
|
795
|
+
interruption: 'none' | 'temporary' | 'system' | 'unknown';
|
|
796
|
+
};
|
|
797
|
+
/** Optional GROUP: a platform that cannot report voice session state omits
|
|
798
|
+
* the whole block rather than guessing `false` for each field. */
|
|
799
|
+
voice?: {
|
|
800
|
+
sessionActive: boolean | null;
|
|
801
|
+
userSpeaking?: boolean | null;
|
|
802
|
+
assistantSpeaking?: boolean | null;
|
|
803
|
+
};
|
|
804
|
+
/** May report that a camera session is running; must NOT duplicate frame
|
|
805
|
+
* state. Frames belong to `visual`. */
|
|
806
|
+
visual?: {
|
|
807
|
+
cameraSessionActive: boolean | null;
|
|
808
|
+
};
|
|
809
|
+
quality: StateQuality;
|
|
810
|
+
}
|
|
811
|
+
/** FROZEN v0.1 (VOCABULARY §"Initial Interaction Events"). All twelve. */
|
|
812
|
+
type InteractionEvent = 'appStateChanged' | 'screenStateChanged' | 'audioRouteChanged' | 'audioInterruptionStarted' | 'audioInterruptionEnded' | 'voiceSessionStarted' | 'voiceSessionStopped' | 'cameraSessionStarted' | 'cameraSessionStopped' | 'userBecameActive' | 'userBecameIdle' | 'interactionStateStale';
|
|
813
|
+
/**
|
|
814
|
+
* NO INTENT TYPE. §10 declares only `InteractionSnapshot`, and the vocabulary
|
|
815
|
+
* — which does publish intents for spatial, visual, activity, connectivity and
|
|
816
|
+
* environment — has no "Interaction Intents" section. Five profiles are
|
|
817
|
+
* floated in the design doc, hedged twice ("Likely profiles, if needed") with
|
|
818
|
+
* no obligation attached to any of them. Not added.
|
|
819
|
+
*/
|
|
820
|
+
interface InteractionDomain {
|
|
821
|
+
getSnapshot(): Promise<InteractionSnapshot>;
|
|
822
|
+
subscribe(listener: StateListener<InteractionEvent>): Subscription;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* FROZEN name (API_FREEZE §11) and FROZEN member names.
|
|
826
|
+
*
|
|
827
|
+
* §11's "Recommended naming rule" says to call it `visual.getSnapshot()`, not
|
|
828
|
+
* `device.visual.*`. Read here as a rule about the WIRE/tool-name convention
|
|
829
|
+
* (`device.*` stays a human/backend routing convention for callable plugin
|
|
830
|
+
* tools) rather than a ban on a plugin-prop container. That reading is a
|
|
831
|
+
* judgement call and is flagged as such — if it is wrong, the fix is to rename
|
|
832
|
+
* the PROP, never to merge these eight members into `PluginNative`.
|
|
833
|
+
*/
|
|
834
|
+
interface DeviceBridgeV2 {
|
|
835
|
+
readonly spatial: SpatialDomain;
|
|
836
|
+
readonly visual: VisualDomain;
|
|
837
|
+
readonly activity: ActivityDomain;
|
|
838
|
+
readonly deviceContext: DeviceContextDomain;
|
|
839
|
+
readonly connectivity: ConnectivityDomain;
|
|
840
|
+
readonly capability: CapabilityDomain;
|
|
841
|
+
readonly environment: EnvironmentDomain;
|
|
842
|
+
readonly interaction: InteractionDomain;
|
|
843
|
+
}
|
|
844
|
+
/** The name plugin authors see. See DESIGN NOTE 1 for why not `deviceBridge`. */
|
|
845
|
+
type DeviceState = DeviceBridgeV2;
|
|
846
|
+
/**
|
|
847
|
+
* DECISION (not in the docs). A second, differently-named manifest field, so a
|
|
848
|
+
* plugin cannot half-declare its way across the boundary:
|
|
849
|
+
*
|
|
850
|
+
* native: { camera: true } → props.device.camera.takePhoto()
|
|
851
|
+
* stateDomains: ['visual', 'spatial'] → props.deviceState.visual.getSnapshot()
|
|
852
|
+
*
|
|
853
|
+
* Declaring `native.camera` grants NOTHING in the semantic layer, and vice
|
|
854
|
+
* versa. There is no field whose meaning spans both.
|
|
855
|
+
*
|
|
856
|
+
* How an UNDECLARED domain must behave — and this is the load-bearing part:
|
|
857
|
+
* it returns a normal snapshot in which every enum is `unknown`, every
|
|
858
|
+
* nullable is `null`, and `quality.freshness` is
|
|
859
|
+
* `{ state:'unknown', observedAt:null, ageMs:null }`. It does NOT throw and it
|
|
860
|
+
* does NOT return undefined.
|
|
861
|
+
*
|
|
862
|
+
* Why that matters here specifically: today's `gatePluginNative` gives an
|
|
863
|
+
* undeclared capability a proxy whose methods `console.warn` and return
|
|
864
|
+
* `undefined` (packages/plugin-runtime/src/host.ts:67-76). So
|
|
865
|
+
* `await native.location.getCurrent()` on an undeclared plugin resolves to
|
|
866
|
+
* `undefined`, and `pos?.latitude` is `undefined` — which then flows into an
|
|
867
|
+
* LLM answer as a confident nothing. A silent undefined is a fabrication
|
|
868
|
+
* vector; a snapshot of explicit unknowns is not.
|
|
869
|
+
*
|
|
870
|
+
* The "why" is readable from `capability.getSnapshot()` using an existing
|
|
871
|
+
* frozen enum member: a manifest-gate denial maps to
|
|
872
|
+
* `restriction: 'policyRestricted'` — the plugin manifest IS a policy. No new
|
|
873
|
+
* vocabulary needed. (The docs never consider a manifest gate, so this
|
|
874
|
+
* mapping is a DECISION, but it uses only frozen values.)
|
|
875
|
+
*/
|
|
876
|
+
type StateDomainDeclaration = readonly DeviceBridgeDomain[];
|
|
877
|
+
/**
|
|
878
|
+
* Is this snapshot good enough to act on?
|
|
879
|
+
*
|
|
880
|
+
* Deliberately requires the caller to state a budget: there is no universal
|
|
881
|
+
* "fresh enough", and the docs give no numeric thresholds for any domain
|
|
882
|
+
* (native chooses sampling behaviour). A guidance loop wants ~1500 ms; a
|
|
883
|
+
* battery-aware background job is happy with 60 s.
|
|
884
|
+
*
|
|
885
|
+
* `unknown` freshness fails: it means nothing was ever observed, which is not
|
|
886
|
+
* the same as an old-but-real reading and must not be treated as one.
|
|
887
|
+
*/
|
|
888
|
+
declare function isActionable(quality: StateQuality, budget: {
|
|
889
|
+
maxAgeMs: number;
|
|
890
|
+
minConfidence?: ConfidenceLevel;
|
|
891
|
+
}): boolean;
|
|
892
|
+
/** The honest "we have never observed this" value. Use it instead of
|
|
893
|
+
* inventing a timestamp when constructing a degraded snapshot. This is the
|
|
894
|
+
* canonical source of the `unknown` freshness shape — the engine's
|
|
895
|
+
* `qualityUnknown()` derives from it so the two never drift. */
|
|
896
|
+
declare function neverObserved(): Freshness;
|
|
897
|
+
/**
|
|
898
|
+
* One human-readable sentence for why a capability cannot answer, built ONLY
|
|
899
|
+
* from what the four axes actually say. Returns null when the capability is
|
|
900
|
+
* fine — so a caller cannot accidentally print a reason that does not exist.
|
|
901
|
+
*/
|
|
902
|
+
declare function explainCapability(status: CapabilityStatus): string | null;
|
|
903
|
+
/**
|
|
904
|
+
* Additive extension of `PluginProps` (sdk/types.ts:259).
|
|
905
|
+
*
|
|
906
|
+
* `deviceState` is OPTIONAL because a host that has not installed a v2
|
|
907
|
+
* provider must not pretend to have one. `undefined` means "this host has no
|
|
908
|
+
* semantic layer", which a plugin can check once; it does NOT mean "the
|
|
909
|
+
* sensors said nothing". Those are different, and collapsing them would
|
|
910
|
+
* reintroduce exactly the ambiguity this file exists to remove.
|
|
911
|
+
*/
|
|
912
|
+
interface PluginPropsV2Extension {
|
|
913
|
+
readonly deviceState?: DeviceState;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Additive extension of `DeviceToolContext` (sdk/types.ts:320-327).
|
|
917
|
+
*
|
|
918
|
+
* This is the seam that matters most: device tools are what the AI calls, and
|
|
919
|
+
* device tools are where a fabricated value reaches a user. The lint rule
|
|
920
|
+
* proposed alongside this file (`no-mechanical-sensing-in-device-tools`) bans
|
|
921
|
+
* `ctx.native.camera`, `ctx.native.location` and `ctx.native.connectivity`
|
|
922
|
+
* inside a `DeviceToolDefinition.run` body — a device tool must reach sensing
|
|
923
|
+
* through `ctx.deviceState`. UI code is unaffected.
|
|
924
|
+
*/
|
|
925
|
+
interface DeviceToolContextV2Extension {
|
|
926
|
+
readonly deviceState?: DeviceState;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
type deviceState_ActivityDomain = ActivityDomain;
|
|
930
|
+
type deviceState_ActivityIntent = ActivityIntent;
|
|
931
|
+
type deviceState_ActivitySnapshot = ActivitySnapshot;
|
|
932
|
+
type deviceState_AssertConforming<T extends ConformingEventName> = AssertConforming<T>;
|
|
933
|
+
type deviceState_AvailabilityState = AvailabilityState;
|
|
934
|
+
type deviceState_CapabilityDomain = CapabilityDomain;
|
|
935
|
+
type deviceState_CapabilityEvent = CapabilityEvent;
|
|
936
|
+
type deviceState_CapabilitySnapshot = CapabilitySnapshot;
|
|
937
|
+
type deviceState_CapabilityStatus = CapabilityStatus;
|
|
938
|
+
type deviceState_Confidence = Confidence;
|
|
939
|
+
type deviceState_ConfidenceLevel = ConfidenceLevel;
|
|
940
|
+
type deviceState_ConformingEventName = ConformingEventName;
|
|
941
|
+
type deviceState_ConnectivityDomain = ConnectivityDomain;
|
|
942
|
+
type deviceState_ConnectivityEvent = ConnectivityEvent;
|
|
943
|
+
type deviceState_ConnectivityIntent = ConnectivityIntent;
|
|
944
|
+
type deviceState_ConnectivitySnapshot = ConnectivitySnapshot;
|
|
945
|
+
type deviceState_DeviceBridgeDomain = DeviceBridgeDomain;
|
|
946
|
+
type deviceState_DeviceBridgeError = DeviceBridgeError;
|
|
947
|
+
declare const deviceState_DeviceBridgeError: typeof DeviceBridgeError;
|
|
948
|
+
type deviceState_DeviceBridgeErrorCode = DeviceBridgeErrorCode;
|
|
949
|
+
type deviceState_DeviceBridgeV2 = DeviceBridgeV2;
|
|
950
|
+
type deviceState_DeviceContextDomain = DeviceContextDomain;
|
|
951
|
+
type deviceState_DeviceContextSnapshot = DeviceContextSnapshot;
|
|
952
|
+
type deviceState_DevicePlacement = DevicePlacement;
|
|
953
|
+
type deviceState_DeviceState = DeviceState;
|
|
954
|
+
type deviceState_DeviceToolContextV2Extension = DeviceToolContextV2Extension;
|
|
955
|
+
type deviceState_EnvironmentDomain = EnvironmentDomain;
|
|
956
|
+
type deviceState_EnvironmentEvent = EnvironmentEvent;
|
|
957
|
+
type deviceState_EnvironmentIntent = EnvironmentIntent;
|
|
958
|
+
type deviceState_EnvironmentSnapshot = EnvironmentSnapshot;
|
|
959
|
+
type deviceState_Freshness = Freshness;
|
|
960
|
+
type deviceState_FreshnessState = FreshnessState;
|
|
961
|
+
type deviceState_HumanActivity = HumanActivity;
|
|
962
|
+
type deviceState_InteractionDomain = InteractionDomain;
|
|
963
|
+
type deviceState_InteractionEvent = InteractionEvent;
|
|
964
|
+
type deviceState_InteractionSnapshot = InteractionSnapshot;
|
|
965
|
+
type deviceState_MediaHandle = MediaHandle;
|
|
966
|
+
type deviceState_PluginPropsV2Extension = PluginPropsV2Extension;
|
|
967
|
+
type deviceState_RestrictionState = RestrictionState;
|
|
968
|
+
type deviceState_SpatialDomain = SpatialDomain;
|
|
969
|
+
type deviceState_SpatialEvent = SpatialEvent;
|
|
970
|
+
type deviceState_SpatialIntent = SpatialIntent;
|
|
971
|
+
type deviceState_SpatialPoint = SpatialPoint;
|
|
972
|
+
type deviceState_SpatialSnapshot = SpatialSnapshot;
|
|
973
|
+
type deviceState_SpatialStateEvent = SpatialStateEvent;
|
|
974
|
+
type deviceState_StartOptions<TIntent extends string> = StartOptions<TIntent>;
|
|
975
|
+
type deviceState_StateDomainDeclaration = StateDomainDeclaration;
|
|
976
|
+
type deviceState_StateEvent<TName extends string = string, TPayload = unknown> = StateEvent<TName, TPayload>;
|
|
977
|
+
type deviceState_StateListener<TName extends string = string> = StateListener<TName>;
|
|
978
|
+
type deviceState_StateQuality = StateQuality;
|
|
979
|
+
type deviceState_StateSession = StateSession;
|
|
980
|
+
type deviceState_StatefulDomain<TSnapshot, TIntent extends string = never, TEvent extends string = string> = StatefulDomain<TSnapshot, TIntent, TEvent>;
|
|
981
|
+
type deviceState_Subscription = Subscription;
|
|
982
|
+
type deviceState_SupportState = SupportState;
|
|
983
|
+
type deviceState_VisualCaptureOptions = VisualCaptureOptions;
|
|
984
|
+
type deviceState_VisualCaptureResult = VisualCaptureResult;
|
|
985
|
+
type deviceState_VisualDomain = VisualDomain;
|
|
986
|
+
type deviceState_VisualFrameQuality = VisualFrameQuality;
|
|
987
|
+
type deviceState_VisualFrameRef = VisualFrameRef;
|
|
988
|
+
type deviceState_VisualIntent = VisualIntent;
|
|
989
|
+
type deviceState_VisualSnapshot = VisualSnapshot;
|
|
990
|
+
declare const deviceState_explainCapability: typeof explainCapability;
|
|
991
|
+
declare const deviceState_isActionable: typeof isActionable;
|
|
992
|
+
declare const deviceState_neverObserved: typeof neverObserved;
|
|
993
|
+
declare namespace deviceState {
|
|
994
|
+
export { type deviceState_ActivityDomain as ActivityDomain, type deviceState_ActivityIntent as ActivityIntent, type deviceState_ActivitySnapshot as ActivitySnapshot, type deviceState_AssertConforming as AssertConforming, type deviceState_AvailabilityState as AvailabilityState, type deviceState_CapabilityDomain as CapabilityDomain, type deviceState_CapabilityEvent as CapabilityEvent, type deviceState_CapabilitySnapshot as CapabilitySnapshot, type deviceState_CapabilityStatus as CapabilityStatus, type deviceState_Confidence as Confidence, type deviceState_ConfidenceLevel as ConfidenceLevel, type deviceState_ConformingEventName as ConformingEventName, type deviceState_ConnectivityDomain as ConnectivityDomain, type deviceState_ConnectivityEvent as ConnectivityEvent, type deviceState_ConnectivityIntent as ConnectivityIntent, type deviceState_ConnectivitySnapshot as ConnectivitySnapshot, type deviceState_DeviceBridgeDomain as DeviceBridgeDomain, deviceState_DeviceBridgeError as DeviceBridgeError, type deviceState_DeviceBridgeErrorCode as DeviceBridgeErrorCode, type deviceState_DeviceBridgeV2 as DeviceBridgeV2, type deviceState_DeviceContextDomain as DeviceContextDomain, type deviceState_DeviceContextSnapshot as DeviceContextSnapshot, type deviceState_DevicePlacement as DevicePlacement, type deviceState_DeviceState as DeviceState, type deviceState_DeviceToolContextV2Extension as DeviceToolContextV2Extension, type deviceState_EnvironmentDomain as EnvironmentDomain, type deviceState_EnvironmentEvent as EnvironmentEvent, type deviceState_EnvironmentIntent as EnvironmentIntent, type deviceState_EnvironmentSnapshot as EnvironmentSnapshot, type deviceState_Freshness as Freshness, type deviceState_FreshnessState as FreshnessState, type deviceState_HumanActivity as HumanActivity, type deviceState_InteractionDomain as InteractionDomain, type deviceState_InteractionEvent as InteractionEvent, type deviceState_InteractionSnapshot as InteractionSnapshot, type deviceState_MediaHandle as MediaHandle, type PermissionState$1 as PermissionState, type deviceState_PluginPropsV2Extension as PluginPropsV2Extension, type deviceState_RestrictionState as RestrictionState, type deviceState_SpatialDomain as SpatialDomain, type deviceState_SpatialEvent as SpatialEvent, type deviceState_SpatialIntent as SpatialIntent, type deviceState_SpatialPoint as SpatialPoint, type deviceState_SpatialSnapshot as SpatialSnapshot, type deviceState_SpatialStateEvent as SpatialStateEvent, type deviceState_StartOptions as StartOptions, type deviceState_StateDomainDeclaration as StateDomainDeclaration, type deviceState_StateEvent as StateEvent, type deviceState_StateListener as StateListener, type deviceState_StateQuality as StateQuality, type deviceState_StateSession as StateSession, type deviceState_StatefulDomain as StatefulDomain, type deviceState_Subscription as Subscription, type deviceState_SupportState as SupportState, type deviceState_VisualCaptureOptions as VisualCaptureOptions, type deviceState_VisualCaptureResult as VisualCaptureResult, type deviceState_VisualDomain as VisualDomain, type deviceState_VisualFrameQuality as VisualFrameQuality, type deviceState_VisualFrameRef as VisualFrameRef, type deviceState_VisualIntent as VisualIntent, type deviceState_VisualSnapshot as VisualSnapshot, deviceState_explainCapability as explainCapability, deviceState_isActionable as isActionable, deviceState_neverObserved as neverObserved };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* props.voice — the live conversational SESSION a plugin may drive.
|
|
999
|
+
*
|
|
1000
|
+
* Voice is a SESSION, not a device sensor. `props.device.*` is mechanical verbs
|
|
1001
|
+
* that throw; `props.deviceState.*` is eight semantic domains describing the
|
|
1002
|
+
* device. A Realtime conversation is neither — it is a long-lived loop with its
|
|
1003
|
+
* own state (connecting/connected), its own transcript, and two ways to take a
|
|
1004
|
+
* turn (the user speaks, or the agent speaks first). So it gets its own gated
|
|
1005
|
+
* surface, mirroring the deviceState GRAMMAR (start / stop / getSnapshot /
|
|
1006
|
+
* subscribe) because a reasoning loop reads it the same way — state with
|
|
1007
|
+
* freshness, not bare values.
|
|
1008
|
+
*
|
|
1009
|
+
* TRUST BOUNDARY. This type is the WHOLE surface. There is deliberately NO way
|
|
1010
|
+
* from here to inject fake user speech, read raw audio, reach the WebView, or
|
|
1011
|
+
* hide the mic-active indicator — those are new doors, and the reason
|
|
1012
|
+
* `CameraPreview` is a component rather than a module is that we keep closing
|
|
1013
|
+
* doors of that shape. `prompt()` is an AGENT turn, not a forged user turn.
|
|
1014
|
+
*
|
|
1015
|
+
* See docs/handoff/SPEC_voice_session_without_the_voice_screen_2026-09-12.md.
|
|
1016
|
+
*/
|
|
1017
|
+
/** The session lifecycle — identical vocabulary to the built-in `CallState`. */
|
|
1018
|
+
type VoiceState = 'idle' | 'connecting' | 'connected' | 'error';
|
|
1019
|
+
/** One line of the conversation. `tool` entries are device/skill tool activity. */
|
|
1020
|
+
interface VoiceTranscriptEntry {
|
|
1021
|
+
role: 'user' | 'ai' | 'tool';
|
|
1022
|
+
text: string;
|
|
1023
|
+
ts: number;
|
|
1024
|
+
}
|
|
1025
|
+
/** A point-in-time read of the session — what a reasoning loop polls. */
|
|
1026
|
+
interface VoiceSnapshot {
|
|
1027
|
+
state: VoiceState;
|
|
1028
|
+
sessionId: string | null;
|
|
1029
|
+
muted: boolean;
|
|
1030
|
+
/** The agent is currently producing audio. */
|
|
1031
|
+
speaking: boolean;
|
|
1032
|
+
/** The user is currently being heard (partial user transcript in flight). */
|
|
1033
|
+
listening: boolean;
|
|
1034
|
+
/** The user is currently speaking, per the session's own VAD (authoritative,
|
|
1035
|
+
* distinct from `listening` which tracks a partial transcript). */
|
|
1036
|
+
userSpeaking: boolean;
|
|
1037
|
+
/**
|
|
1038
|
+
* Epoch ms of the last moment the user STOPPED speaking, or `null` if they
|
|
1039
|
+
* have not spoken yet this session. `null` is deliberate and load-bearing: a
|
|
1040
|
+
* silence timer must arm on the first real `speaking:false`, never at session
|
|
1041
|
+
* start — "not speaking" and "has never spoken" are different, and arming on
|
|
1042
|
+
* the wrong one fires before anyone has said anything.
|
|
1043
|
+
*/
|
|
1044
|
+
lastUserSpeechEndedAt: number | null;
|
|
1045
|
+
/** The last error, if `state === 'error'`; also carries a refusal reason when
|
|
1046
|
+
* this surface was handed to a plugin that did not declare `voice`. */
|
|
1047
|
+
lastError: string | null;
|
|
1048
|
+
/** The most recent LOCAL device-tool call the model made through the session
|
|
1049
|
+
* (name + epoch ms), or null if none yet. Diagnostic: proves whether the
|
|
1050
|
+
* model's call_device_tool actually reached the phone vs was never made. */
|
|
1051
|
+
lastDeviceTool: {
|
|
1052
|
+
name: string;
|
|
1053
|
+
at: number;
|
|
1054
|
+
} | null;
|
|
1055
|
+
/** The sessionId the Core allowset is bound to. Diagnostic: authorization
|
|
1056
|
+
* requires it to equal `sessionId`; a mismatch is why tools refuse. */
|
|
1057
|
+
authorizedSessionId: string | null;
|
|
1058
|
+
}
|
|
1059
|
+
interface VoiceStartOptions {
|
|
1060
|
+
/** Hand the mic to a specific skill on connect (voice /session → takeMic),
|
|
1061
|
+
* exactly as `sys.focusUiPlugin("platform:voice", { skillSlug })` does. */
|
|
1062
|
+
skillSlug?: string;
|
|
1063
|
+
/**
|
|
1064
|
+
* A PER-CALL persona override, same shape as the authored
|
|
1065
|
+
* `solution.voice.persona` ({ name, style }). Declared on the voice
|
|
1066
|
+
* session-create call (POST /session `persona`); the server sanitizes it
|
|
1067
|
+
* (name/style only, length-capped) and layers it OVER the solution's authored
|
|
1068
|
+
* persona for THIS call only — prompt-layer, so it can never touch caller
|
|
1069
|
+
* verification or tool allow-lists. Set once at session start and constant
|
|
1070
|
+
* for the call, so it adds no per-turn prompt pushes.
|
|
1071
|
+
*
|
|
1072
|
+
* Use it when a plugin needs its own voice persona without owning a skill
|
|
1073
|
+
* (e.g. a guide HUD). A solution-wide persona belongs in solution.voice.
|
|
1074
|
+
*/
|
|
1075
|
+
persona?: VoicePersona;
|
|
1076
|
+
/**
|
|
1077
|
+
* `'none'` = a CLEAN call: the voice gets none of the workspace's skills — no
|
|
1078
|
+
* skill manifest, no skill tools, no platform welcome line, no mid-call
|
|
1079
|
+
* skill-state prompt refresh — only the voice basics, this call's persona and
|
|
1080
|
+
* the CALLING plugin's own device tools (other solutions' tools registered on
|
|
1081
|
+
* the same phone are neither offered nor run). For a plugin that owns its conversation (e.g.
|
|
1082
|
+
* a guide HUD). Reduce-only: it can remove what the voice gets, never add.
|
|
1083
|
+
* Absent = the workspace's skills, as today. Not combinable with `skillSlug`.
|
|
1084
|
+
*/
|
|
1085
|
+
skills?: 'none';
|
|
1086
|
+
}
|
|
1087
|
+
/** The authored persona shape (solution.voice.persona). */
|
|
1088
|
+
interface VoicePersona {
|
|
1089
|
+
/** The name the voice uses for itself ("You are <name> …"). */
|
|
1090
|
+
name?: string;
|
|
1091
|
+
/** Free-text style/behaviour guidance rendered under the identity line. */
|
|
1092
|
+
style?: string;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* What changed. Coarse on purpose: `state` covers connect/mute/session/partials,
|
|
1096
|
+
* `transcript` fires when a finalized line was appended. A subscriber re-reads
|
|
1097
|
+
* `getSnapshot()` / `transcript()` — the event is a nudge, not a payload, so the
|
|
1098
|
+
* surface never has to keep two shapes of the same fact in sync.
|
|
1099
|
+
*/
|
|
1100
|
+
type VoiceEvent = {
|
|
1101
|
+
type: 'state';
|
|
1102
|
+
} | {
|
|
1103
|
+
type: 'transcript';
|
|
1104
|
+
};
|
|
1105
|
+
interface VoiceSubscription {
|
|
1106
|
+
unsubscribe(): void;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* WHO is asking. The one physical session has an OWNER; access is scoped to it.
|
|
1110
|
+
*
|
|
1111
|
+
* A plugin's caller is its `pluginId` (a string), stamped by gateVoice so the
|
|
1112
|
+
* plugin never supplies its own identity. The PLATFORM caller — the built-in
|
|
1113
|
+
* voice screen, the user's own surface — is a Symbol, deliberately: a Symbol can
|
|
1114
|
+
* never equal any pluginId string, so a plugin cannot forge platform access by
|
|
1115
|
+
* naming itself `platform:voice`.
|
|
1116
|
+
*/
|
|
1117
|
+
type VoiceCaller = string | symbol;
|
|
1118
|
+
/** The privileged caller — the platform's own surfaces (the built-in sheet, the
|
|
1119
|
+
* mic indicator, the session host). Sees and controls any live session, because
|
|
1120
|
+
* it is the USER's direct surface onto their own conversation. Unforgeable. */
|
|
1121
|
+
declare const PLATFORM_VOICE_CALLER: unique symbol;
|
|
1122
|
+
/**
|
|
1123
|
+
* The OWNER-AWARE session the HOST implements and hands to setVoiceProvider.
|
|
1124
|
+
*
|
|
1125
|
+
* This is NOT what a plugin receives — a plugin gets a `VoiceAPI` facade
|
|
1126
|
+
* (gateVoice) that stamps its `pluginId` as the caller on every call, so the
|
|
1127
|
+
* provider can enforce that a plugin only ever reads, controls, or attaches to a
|
|
1128
|
+
* session IT owns. One physical session, isolated per authorized identity.
|
|
1129
|
+
*/
|
|
1130
|
+
interface VoiceSessionProvider {
|
|
1131
|
+
start(opts: VoiceStartOptions | undefined, caller: VoiceCaller): Promise<VoiceSnapshot>;
|
|
1132
|
+
stop(caller: VoiceCaller): void;
|
|
1133
|
+
setMuted(muted: boolean, caller: VoiceCaller): void;
|
|
1134
|
+
prompt(input: string, caller: VoiceCaller): void;
|
|
1135
|
+
observe(text: string, caller: VoiceCaller): void;
|
|
1136
|
+
getSnapshot(caller: VoiceCaller): VoiceSnapshot;
|
|
1137
|
+
subscribe(cb: (event: VoiceEvent) => void, caller: VoiceCaller): VoiceSubscription;
|
|
1138
|
+
transcript(caller: VoiceCaller): VoiceTranscriptEntry[];
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* The gated surface a plugin receives as `props.voice`. Present on EVERY plugin;
|
|
1142
|
+
* a plugin that did not declare `capabilities: { voice: true }` gets a version
|
|
1143
|
+
* whose reads say so and whose actions throw — never a silent no-op, never a
|
|
1144
|
+
* fake `idle` (see gateVoice in voiceHost.ts).
|
|
1145
|
+
*/
|
|
1146
|
+
interface VoiceAPI {
|
|
1147
|
+
/** Open the live loop. Resolves once connected (or rejects on connect error).
|
|
1148
|
+
* If a session is ALREADY live, ATTACHES to it and resolves immediately —
|
|
1149
|
+
* never a second Realtime loop (one phone, one mic, one bill). */
|
|
1150
|
+
start(opts?: VoiceStartOptions): Promise<VoiceSnapshot>;
|
|
1151
|
+
/** End the live loop. Safe to call when idle. */
|
|
1152
|
+
stop(): void;
|
|
1153
|
+
/** Mute/unmute the microphone. The session stays live; the mic indicator
|
|
1154
|
+
* reflects it. */
|
|
1155
|
+
setMuted(muted: boolean): void;
|
|
1156
|
+
/**
|
|
1157
|
+
* Make the agent take a turn WITHOUT the user speaking — the guide speaking
|
|
1158
|
+
* first. `input` is the instruction for that turn. This is an agent-initiated
|
|
1159
|
+
* turn into the SAME session, NOT fabricated user speech: one session, two
|
|
1160
|
+
* trigger sources. Throws if the session is not connected.
|
|
1161
|
+
*/
|
|
1162
|
+
prompt(input: string): void;
|
|
1163
|
+
/**
|
|
1164
|
+
* Stream a silent OBSERVATION into the session — tell the model something
|
|
1165
|
+
* without asking it to speak (a `system` conversation item). Cheap and
|
|
1166
|
+
* high-frequency (safe once or twice a second): this is the channel for what
|
|
1167
|
+
* the camera sees while the user walks. The model answers with the
|
|
1168
|
+
* observations in hand the next time it takes a turn (user VAD, or prompt()) —
|
|
1169
|
+
* an observation never triggers a turn by itself. Throws if not connected.
|
|
1170
|
+
*/
|
|
1171
|
+
observe(text: string): void;
|
|
1172
|
+
/** Point-in-time session state. */
|
|
1173
|
+
getSnapshot(): VoiceSnapshot;
|
|
1174
|
+
/** Subscribe to state + transcript changes. Returns an unsubscribe handle. */
|
|
1175
|
+
subscribe(cb: (event: VoiceEvent) => void): VoiceSubscription;
|
|
1176
|
+
/** The conversation so far. */
|
|
1177
|
+
transcript(): VoiceTranscriptEntry[];
|
|
1178
|
+
}
|
|
1179
|
+
|
|
8
1180
|
/**
|
|
9
1181
|
* @adas/plugin-sdk — Type definitions
|
|
10
1182
|
*
|
|
11
1183
|
* This is the developer contract for native mobile plugins.
|
|
12
1184
|
* Both UI and service plugins use these interfaces.
|
|
13
1185
|
*/
|
|
1186
|
+
|
|
14
1187
|
interface PluginContext {
|
|
15
1188
|
tenant: string;
|
|
16
1189
|
pluginId: string;
|
|
@@ -59,6 +1232,11 @@ interface CameraAPI {
|
|
|
59
1232
|
width: number;
|
|
60
1233
|
height: number;
|
|
61
1234
|
}>;
|
|
1235
|
+
/**
|
|
1236
|
+
* @deprecated Use `native.scanner.scanOnce()`. Scanning needs a MOUNTED
|
|
1237
|
+
* camera, which a bare function call cannot provide; this refuses by name
|
|
1238
|
+
* and points there.
|
|
1239
|
+
*/
|
|
62
1240
|
scanBarcode(opts?: {
|
|
63
1241
|
formats?: string[];
|
|
64
1242
|
}): Promise<{
|
|
@@ -70,6 +1248,26 @@ interface CameraAPI {
|
|
|
70
1248
|
}): Promise<{
|
|
71
1249
|
uri: string;
|
|
72
1250
|
}[]>;
|
|
1251
|
+
/**
|
|
1252
|
+
* Record a video clip the user deliberately makes — a damage claim, a
|
|
1253
|
+
* how-it-sounds clip, something to attach.
|
|
1254
|
+
*
|
|
1255
|
+
* NOT the Visual domain. That is an ephemeral frame ring a reasoning loop
|
|
1256
|
+
* reads and nobody keeps; this produces a FILE. Different jobs, and the
|
|
1257
|
+
* grammar says so: this is a verb that throws, Visual is a state read that
|
|
1258
|
+
* says `unknown`.
|
|
1259
|
+
*
|
|
1260
|
+
* Rides the same mounted camera surface as `scanner.scanOnce`, and throws
|
|
1261
|
+
* the same coded failures. `withAudio` needs microphone permission and
|
|
1262
|
+
* REFUSES without it rather than quietly recording silence.
|
|
1263
|
+
*/
|
|
1264
|
+
recordVideo(opts?: {
|
|
1265
|
+
maxDurationMs?: number;
|
|
1266
|
+
withAudio?: boolean;
|
|
1267
|
+
}): Promise<{
|
|
1268
|
+
uri: string;
|
|
1269
|
+
durationMs: number | null;
|
|
1270
|
+
}>;
|
|
73
1271
|
}
|
|
74
1272
|
interface LocationAPI {
|
|
75
1273
|
getCurrent(opts?: {
|
|
@@ -124,6 +1322,16 @@ interface HapticsAPI {
|
|
|
124
1322
|
impact(style: 'light' | 'medium' | 'heavy'): void;
|
|
125
1323
|
notification(type: 'success' | 'warning' | 'error'): void;
|
|
126
1324
|
selection(): void;
|
|
1325
|
+
/**
|
|
1326
|
+
* Shorthand for notification('error').
|
|
1327
|
+
*
|
|
1328
|
+
* Documented in the platform guide and already CALLED by shipped plugins
|
|
1329
|
+
* (connections-panel), which only compiled because they reached it through
|
|
1330
|
+
* optional chaining — `native?.haptics?.error?.()` silently does nothing when
|
|
1331
|
+
* the method is absent, so a failure the user was meant to FEEL produced no
|
|
1332
|
+
* buzz and no error. Declared here so the call site is checked.
|
|
1333
|
+
*/
|
|
1334
|
+
error(): void;
|
|
127
1335
|
}
|
|
128
1336
|
interface ConnectivityAPI {
|
|
129
1337
|
isOnline(): boolean;
|
|
@@ -177,6 +1385,29 @@ interface ScannerAPI {
|
|
|
177
1385
|
granted: boolean;
|
|
178
1386
|
status: string;
|
|
179
1387
|
}>;
|
|
1388
|
+
/**
|
|
1389
|
+
* Show the scanner and resolve with the FIRST code read.
|
|
1390
|
+
*
|
|
1391
|
+
* The plugin no longer mounts anything: the host renders one
|
|
1392
|
+
* `CameraTaskHost`, this raises it, and the promise settles when a code is
|
|
1393
|
+
* found. That is why the capture lifecycle is owned in exactly one place
|
|
1394
|
+
* instead of by every plugin that wants to scan.
|
|
1395
|
+
*
|
|
1396
|
+
* THROWS rather than resolving empty, with a code that says which:
|
|
1397
|
+
* TIMEOUT nothing was found in time
|
|
1398
|
+
* CANCELLED the user dismissed it — a choice, not a fault
|
|
1399
|
+
* PERMISSION_DENIED camera permission refused
|
|
1400
|
+
* BUSY another camera task is running
|
|
1401
|
+
* UNAVAILABLE no camera surface is mounted in this app
|
|
1402
|
+
* An empty string would read as a code that scanned to nothing.
|
|
1403
|
+
*/
|
|
1404
|
+
scanOnce(opts?: {
|
|
1405
|
+
formats?: string[];
|
|
1406
|
+
timeoutMs?: number;
|
|
1407
|
+
}): Promise<{
|
|
1408
|
+
data: string;
|
|
1409
|
+
format: string;
|
|
1410
|
+
}>;
|
|
180
1411
|
}
|
|
181
1412
|
interface NfcNdefRecord {
|
|
182
1413
|
type: string;
|
|
@@ -212,10 +1443,235 @@ interface BiometricsAPIv2 {
|
|
|
212
1443
|
error?: string;
|
|
213
1444
|
}>;
|
|
214
1445
|
}
|
|
1446
|
+
interface BluetoothDevice {
|
|
1447
|
+
id: string;
|
|
1448
|
+
name: string | null;
|
|
1449
|
+
/** Signal strength in dBm. Null when not reported — never 0, which is a
|
|
1450
|
+
* legitimate (and very strong) reading. */
|
|
1451
|
+
rssi: number | null;
|
|
1452
|
+
/** Manufacturer/service payload, if the advertisement carried one. */
|
|
1453
|
+
serviceUUIDs?: string[];
|
|
1454
|
+
}
|
|
1455
|
+
interface VisionText {
|
|
1456
|
+
/** The recognised string. */
|
|
1457
|
+
text: string;
|
|
1458
|
+
/** 0..1 recogniser confidence. Null when the platform gives no estimate —
|
|
1459
|
+
* never 1, which would read as certainty nothing measured. */
|
|
1460
|
+
confidence: number | null;
|
|
1461
|
+
/** Normalised 0..1 box in the frame, origin top-left. Lets a guide say
|
|
1462
|
+
* "the sign on your right" rather than just quoting words. */
|
|
1463
|
+
box?: {
|
|
1464
|
+
x: number;
|
|
1465
|
+
y: number;
|
|
1466
|
+
width: number;
|
|
1467
|
+
height: number;
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
interface VisionLabel {
|
|
1471
|
+
/** A label from the platform's classifier vocabulary — NOT free text. */
|
|
1472
|
+
label: string;
|
|
1473
|
+
confidence: number | null;
|
|
1474
|
+
}
|
|
1475
|
+
interface VisionObject {
|
|
1476
|
+
/** A detector-vocabulary class name (e.g. "Car", "Traffic light", "Person") —
|
|
1477
|
+
* NOT free text. */
|
|
1478
|
+
label: string;
|
|
1479
|
+
/** 0..1 detector confidence. Null when the platform gives no estimate — never
|
|
1480
|
+
* 1, which would read as certainty nothing measured. */
|
|
1481
|
+
confidence: number | null;
|
|
1482
|
+
/** Normalised 0..1 box in the frame, origin top-left. Where the thing is,
|
|
1483
|
+
* so a guide can say "on your right" rather than just naming it. */
|
|
1484
|
+
box?: {
|
|
1485
|
+
x: number;
|
|
1486
|
+
y: number;
|
|
1487
|
+
width: number;
|
|
1488
|
+
height: number;
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* How usable a frame is, as measured rather than guessed.
|
|
1493
|
+
*
|
|
1494
|
+
* This one is the exception to the split above: it is a MEASUREMENT of the
|
|
1495
|
+
* image, not an interpretation of the scene, so its results belong on
|
|
1496
|
+
* `VisualFrameRef.quality` — which currently reports `unknown` for all three
|
|
1497
|
+
* because nothing looks at the pixels. Wiring this is what lets a guide say
|
|
1498
|
+
* "it's too dark for me to tell" instead of describing a black frame.
|
|
1499
|
+
*/
|
|
1500
|
+
interface VisionFrameQuality {
|
|
1501
|
+
blur: 'sharp' | 'slight' | 'severe' | 'unknown';
|
|
1502
|
+
exposure: 'good' | 'dark' | 'bright' | 'unknown';
|
|
1503
|
+
/** True when the lens is covered. Null = not assessed. */
|
|
1504
|
+
obstructed: boolean | null;
|
|
1505
|
+
}
|
|
1506
|
+
interface VisionAPI {
|
|
1507
|
+
/** Is on-device vision available at all? False is a real answer. */
|
|
1508
|
+
isSupported(): Promise<boolean>;
|
|
1509
|
+
/**
|
|
1510
|
+
* Read text in a frame (OCR).
|
|
1511
|
+
*
|
|
1512
|
+
* Returns an EMPTY ARRAY when it looked and found no text — that is a real
|
|
1513
|
+
* finding, distinct from throwing, which means it could not look.
|
|
1514
|
+
*/
|
|
1515
|
+
readText(frameId: string, opts?: {
|
|
1516
|
+
languages?: string[];
|
|
1517
|
+
}): Promise<VisionText[]>;
|
|
1518
|
+
/**
|
|
1519
|
+
* Label what is in a frame.
|
|
1520
|
+
*
|
|
1521
|
+
* Deliberately labels, not prose. A sentence is the SKILL's job — this layer
|
|
1522
|
+
* must not put words in the model's mouth, and a classifier that returns
|
|
1523
|
+
* "a bathroom sign ahead on the left" is asserting a spatial relationship it
|
|
1524
|
+
* did not measure.
|
|
1525
|
+
*/
|
|
1526
|
+
describe(frameId: string, opts?: {
|
|
1527
|
+
maxLabels?: number;
|
|
1528
|
+
}): Promise<VisionLabel[]>;
|
|
1529
|
+
/**
|
|
1530
|
+
* Detect the objects in a frame, each with a box.
|
|
1531
|
+
*
|
|
1532
|
+
* Distinct from `describe`: describe LABELS the whole scene ("kitchen"),
|
|
1533
|
+
* detectObjects LOCATES the things in it ("Person" on the left, "Car" ahead) —
|
|
1534
|
+
* which is what a guidance loop reads to say where something is. Returns an
|
|
1535
|
+
* EMPTY ARRAY when it looked and found nothing, distinct from throwing (could
|
|
1536
|
+
* not look). On-device (bundled detector); frames never leave the phone.
|
|
1537
|
+
*/
|
|
1538
|
+
detectObjects(frameId: string, opts?: {
|
|
1539
|
+
maxItems?: number;
|
|
1540
|
+
minConfidence?: number;
|
|
1541
|
+
}): Promise<VisionObject[]>;
|
|
1542
|
+
/** Measure whether a frame is usable. Feeds VisualFrameRef.quality. */
|
|
1543
|
+
assessQuality(frameId: string): Promise<VisionFrameQuality>;
|
|
1544
|
+
}
|
|
1545
|
+
/** The same `{ latitude, longitude }` `location.getCurrent` returns, so a fix
|
|
1546
|
+
* passes straight in. */
|
|
1547
|
+
interface MapsCoordinate {
|
|
1548
|
+
latitude: number;
|
|
1549
|
+
longitude: number;
|
|
1550
|
+
}
|
|
1551
|
+
/** A postal address, field by field. `null` = the map data has no value — never
|
|
1552
|
+
* an empty string, which would read as a street with no name. */
|
|
1553
|
+
interface MapsAddress {
|
|
1554
|
+
/** The placemark's own name — a building, or often number + street. */
|
|
1555
|
+
name: string | null;
|
|
1556
|
+
street: string | null;
|
|
1557
|
+
streetNumber: string | null;
|
|
1558
|
+
/** Neighbourhood / sub-locality. */
|
|
1559
|
+
district: string | null;
|
|
1560
|
+
city: string | null;
|
|
1561
|
+
/** State / province / administrative area. */
|
|
1562
|
+
region: string | null;
|
|
1563
|
+
postalCode: string | null;
|
|
1564
|
+
country: string | null;
|
|
1565
|
+
/** ISO 3166-1 alpha-2, e.g. "GB". */
|
|
1566
|
+
isoCountryCode: string | null;
|
|
1567
|
+
}
|
|
1568
|
+
interface MapsPlace {
|
|
1569
|
+
/** What the map calls it ("Boots", "10 Downing St"). Null when unnamed. */
|
|
1570
|
+
name: string | null;
|
|
1571
|
+
coordinate: MapsCoordinate;
|
|
1572
|
+
address: MapsAddress;
|
|
1573
|
+
/** Apple's point-of-interest category as a vocabulary term ("Pharmacy",
|
|
1574
|
+
* "Cafe", "PublicTransport") — NOT free text. Null for a plain address, or
|
|
1575
|
+
* when the service gives none. */
|
|
1576
|
+
category: string | null;
|
|
1577
|
+
/** Straight-line metres from `opts.near`, when you passed one — NOT walking
|
|
1578
|
+
* distance (ask `route` for that). Null without `near`. */
|
|
1579
|
+
distanceM: number | null;
|
|
1580
|
+
}
|
|
1581
|
+
interface MapsRouteStep {
|
|
1582
|
+
/** Apple's instruction ("Turn right onto Baker St"). The FIRST step is usually
|
|
1583
|
+
* an origin step with an empty instruction — kept, not dropped, so step
|
|
1584
|
+
* indexes line up with Apple's. */
|
|
1585
|
+
instructions: string;
|
|
1586
|
+
distanceM: number;
|
|
1587
|
+
/** An advisory Apple attaches to this step. Null when there is none. */
|
|
1588
|
+
notice: string | null;
|
|
1589
|
+
polyline: MapsCoordinate[];
|
|
1590
|
+
}
|
|
1591
|
+
interface MapsRoute {
|
|
1592
|
+
mode: 'walking';
|
|
1593
|
+
distanceM: number;
|
|
1594
|
+
/** Apple's estimate for the whole route. */
|
|
1595
|
+
expectedTravelTimeMs: number;
|
|
1596
|
+
steps: MapsRouteStep[];
|
|
1597
|
+
/** The whole route, start to end. */
|
|
1598
|
+
polyline: MapsCoordinate[];
|
|
1599
|
+
}
|
|
1600
|
+
interface MapsAPI {
|
|
1601
|
+
/** Can this build search and route? True when the AdaMaps module is in the
|
|
1602
|
+
* binary (iOS). False on Android and on an older build — `reverseGeocode`
|
|
1603
|
+
* does not need the module and may still work there. */
|
|
1604
|
+
isSupported(): Promise<boolean>;
|
|
1605
|
+
/**
|
|
1606
|
+
* Find places by free text — "pharmacy", "10 Downing Street".
|
|
1607
|
+
*
|
|
1608
|
+
* `near` centres the search on a coordinate YOU supply, within `radiusM`
|
|
1609
|
+
* (default 3000, above 0 and at most 100 000). On iOS 18+ results are
|
|
1610
|
+
* confined to that area; before 18 it is a strong hint and a farther result
|
|
1611
|
+
* can come back — each result's `distanceM` says how far. At most `limit`
|
|
1612
|
+
* results (default 8, from 1; more than 25 is capped at 25).
|
|
1613
|
+
*
|
|
1614
|
+
* Network, Apple's servers. `[]` = Apple looked and found nothing. iOS only:
|
|
1615
|
+
* throws UNAVAILABLE on Android or a build without AdaMaps.
|
|
1616
|
+
*/
|
|
1617
|
+
search(query: string, opts?: {
|
|
1618
|
+
near?: MapsCoordinate;
|
|
1619
|
+
radiusM?: number;
|
|
1620
|
+
limit?: number;
|
|
1621
|
+
}): Promise<MapsPlace[]>;
|
|
1622
|
+
/**
|
|
1623
|
+
* What is at a coordinate — its address and name.
|
|
1624
|
+
*
|
|
1625
|
+
* iOS AND Android (the platform geocoder); needs no AdaMaps module. The
|
|
1626
|
+
* result's `coordinate` is the point you asked about, and its
|
|
1627
|
+
* `category` / `distanceM` are null. `null` = nothing there.
|
|
1628
|
+
*
|
|
1629
|
+
* Network, not on-device. iOS reports a rate limit and a lost connection as
|
|
1630
|
+
* the SAME error, so OFFLINE here can also mean "too many requests". Android's
|
|
1631
|
+
* geocoder refuses without location permission (UNAVAILABLE) — that is the
|
|
1632
|
+
* platform's rule, not a location read.
|
|
1633
|
+
*/
|
|
1634
|
+
reverseGeocode(at: MapsCoordinate): Promise<MapsPlace | null>;
|
|
1635
|
+
/**
|
|
1636
|
+
* A walking route between two coordinates YOU supply.
|
|
1637
|
+
*
|
|
1638
|
+
* `null` = Apple found no route — never a fake 0 m route, which would read as
|
|
1639
|
+
* "you have arrived". Walking is the only mode; any other refuses by name.
|
|
1640
|
+
* Network, Apple's servers. iOS only: throws UNAVAILABLE on Android or a build
|
|
1641
|
+
* without AdaMaps.
|
|
1642
|
+
*/
|
|
1643
|
+
route(from: MapsCoordinate, to: MapsCoordinate, opts?: {
|
|
1644
|
+
mode?: 'walking';
|
|
1645
|
+
}): Promise<MapsRoute | null>;
|
|
1646
|
+
}
|
|
1647
|
+
interface BluetoothAPI {
|
|
1648
|
+
/** Is BLE hardware present and permitted? */
|
|
1649
|
+
isSupported(): Promise<boolean>;
|
|
1650
|
+
/** Is the radio switched on? (Separate from permission — both can block.) */
|
|
1651
|
+
isEnabled(): Promise<boolean>;
|
|
1652
|
+
/**
|
|
1653
|
+
* Scan for advertising devices for `timeoutMs`, then stop.
|
|
1654
|
+
*
|
|
1655
|
+
* Bounded rather than open-ended on purpose: a scan left running is a
|
|
1656
|
+
* battery drain the caller cannot see, and the mistake is invisible until a
|
|
1657
|
+
* user notices their phone is flat.
|
|
1658
|
+
*/
|
|
1659
|
+
scan(opts?: {
|
|
1660
|
+
timeoutMs?: number;
|
|
1661
|
+
serviceUUIDs?: string[];
|
|
1662
|
+
}): Promise<BluetoothDevice[]>;
|
|
1663
|
+
}
|
|
215
1664
|
interface PluginNative {
|
|
216
1665
|
camera: CameraAPI;
|
|
217
1666
|
location: LocationAPI;
|
|
218
|
-
|
|
1667
|
+
/**
|
|
1668
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
1669
|
+
* `storageFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
1670
|
+
* single shared storage across plugins was a cross-plugin read/write hole;
|
|
1671
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
1672
|
+
* satisfy the type; new providers implement `storageFor`.
|
|
1673
|
+
*/
|
|
1674
|
+
storage?: StorageAPI;
|
|
219
1675
|
/** @deprecated use `biometricsV2` for the native-backed API. This stub
|
|
220
1676
|
* remains for backwards compatibility. */
|
|
221
1677
|
biometrics: BiometricsAPI;
|
|
@@ -223,11 +1679,44 @@ interface PluginNative {
|
|
|
223
1679
|
notifications: NotificationsAPI;
|
|
224
1680
|
haptics: HapticsAPI;
|
|
225
1681
|
connectivity: ConnectivityAPI;
|
|
226
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* @deprecated The gate never hands this out — it uses the per-plugin
|
|
1684
|
+
* `fileSystemFor(pluginId)` and FAILS CLOSED (blocks) when it can't scope. A
|
|
1685
|
+
* single shared filesystem across plugins was a cross-plugin read/write hole;
|
|
1686
|
+
* there is no shared bucket any more. Present only so older providers still
|
|
1687
|
+
* satisfy the type; new providers implement `fileSystemFor`.
|
|
1688
|
+
*/
|
|
1689
|
+
fileSystem?: FileSystemAPI;
|
|
1690
|
+
/**
|
|
1691
|
+
* Per-plugin filesystem: returns a FileSystemAPI whose sandbox root is scoped
|
|
1692
|
+
* to THIS plugin (`plugin-files/<pluginId>/`), so one plugin cannot read,
|
|
1693
|
+
* write or reach another plugin's files even via traversal. `gatePluginNative`
|
|
1694
|
+
* uses it (passing the plugin's id) to isolate each plugin. If it is absent or
|
|
1695
|
+
* no id is known, the gate FAILS CLOSED (blocks) rather than fall back to a
|
|
1696
|
+
* shared filesystem — there is no shared bucket.
|
|
1697
|
+
*/
|
|
1698
|
+
fileSystemFor?(pluginId: string): FileSystemAPI;
|
|
1699
|
+
/**
|
|
1700
|
+
* Per-plugin storage: returns a StorageAPI whose KV namespace AND SQLite
|
|
1701
|
+
* database are scoped to THIS plugin, so one plugin cannot read, overwrite,
|
|
1702
|
+
* or query another plugin's key-value entries or tables. `gatePluginNative`
|
|
1703
|
+
* uses it (passing the plugin's id) to isolate each plugin. Identity is
|
|
1704
|
+
* threaded through the TRUSTED provider here — never a caller-supplied
|
|
1705
|
+
* key/table prefix. If it is absent or no id is known, the gate FAILS CLOSED
|
|
1706
|
+
* (blocks) rather than fall back to a shared store — there is no shared bucket.
|
|
1707
|
+
*/
|
|
1708
|
+
storageFor?(pluginId: string): StorageAPI;
|
|
227
1709
|
cookies: CookiesAPI;
|
|
228
1710
|
sharing: SharingAPI;
|
|
229
1711
|
scanner: ScannerAPI;
|
|
230
1712
|
nfc: NfcAPI;
|
|
1713
|
+
/** SHAPE ONLY in this build — see BluetoothAPI. Present so it fails by name. */
|
|
1714
|
+
bluetooth: BluetoothAPI;
|
|
1715
|
+
/** SHAPE ONLY in this build — see VisionAPI. On-device image understanding. */
|
|
1716
|
+
vision: VisionAPI;
|
|
1717
|
+
/** Place search, reverse geocoding, walking routes — see MapsAPI. Sends
|
|
1718
|
+
* queries to Apple's map servers; NOT on-device. */
|
|
1719
|
+
maps: MapsAPI;
|
|
231
1720
|
}
|
|
232
1721
|
interface NativeCapabilities$1 {
|
|
233
1722
|
camera?: boolean;
|
|
@@ -237,7 +1726,20 @@ interface NativeCapabilities$1 {
|
|
|
237
1726
|
backgroundTasks?: boolean;
|
|
238
1727
|
fileSystem?: boolean;
|
|
239
1728
|
notifications?: boolean;
|
|
1729
|
+
/** BLE. Declaring it passes the gate but every call refuses by name until
|
|
1730
|
+
* the native side exists — see BluetoothAPI. */
|
|
240
1731
|
bluetooth?: boolean;
|
|
1732
|
+
/** On-device image understanding (OCR, labels, frame quality). Refuses by
|
|
1733
|
+
* name until the native module lands — see VisionAPI. */
|
|
1734
|
+
vision?: boolean;
|
|
1735
|
+
/** Place search, reverse geocoding, walking routes. Means "may send place
|
|
1736
|
+
* queries to Apple Maps" — network, not on-device. See MapsAPI. */
|
|
1737
|
+
maps?: boolean;
|
|
1738
|
+
/** The live voice SESSION (props.voice). A privacy surface — a declaring
|
|
1739
|
+
* plugin can read the conversation transcript — so it is gated like any
|
|
1740
|
+
* other capability, and coupled to the platform's unsuppressable mic
|
|
1741
|
+
* indicator. See VoiceAPI. */
|
|
1742
|
+
voice?: boolean;
|
|
241
1743
|
haptics?: boolean;
|
|
242
1744
|
/** Read the platform cookie jar after a WebView login flow. */
|
|
243
1745
|
cookies?: boolean;
|
|
@@ -305,7 +1807,43 @@ interface ThemeTokens {
|
|
|
305
1807
|
}
|
|
306
1808
|
interface PluginProps {
|
|
307
1809
|
bridge: PluginBridge;
|
|
308
|
-
|
|
1810
|
+
/**
|
|
1811
|
+
* L2 Device Primitives — the camera/location/storage/haptics/… surface, gated
|
|
1812
|
+
* to the capabilities this plugin declared. This is the canonical name (R4);
|
|
1813
|
+
* the underlying L1 native modules keep the `native` name, but what a plugin
|
|
1814
|
+
* receives is `props.device`.
|
|
1815
|
+
*/
|
|
1816
|
+
device: PluginNative;
|
|
1817
|
+
/**
|
|
1818
|
+
* @deprecated Renamed to `device` in R4. Still passed (the SAME gated object)
|
|
1819
|
+
* so existing/remote plugin bundles keep working — migrate to `props.device`.
|
|
1820
|
+
*/
|
|
1821
|
+
native?: PluginNative;
|
|
1822
|
+
/**
|
|
1823
|
+
* L3 DeviceState — the SEMANTIC layer (`props.deviceState.spatial/visual/…`),
|
|
1824
|
+
* gated to the domains this plugin declared in `stateDomains`. Reads answer
|
|
1825
|
+
* `unknown` rather than throwing; a domain the build has no engine for, or the
|
|
1826
|
+
* plugin did not declare, returns an explicit refusal.
|
|
1827
|
+
*
|
|
1828
|
+
* REQUIRED, deliberately: an optional field is exactly the compile-time escape
|
|
1829
|
+
* hatch that let a renderer ship without it (props.deviceState === undefined,
|
|
1830
|
+
* which crashed on drawer/card). Every renderer builds this through
|
|
1831
|
+
* resolvePluginRuntimeProps, so the constructor side always has it; making it
|
|
1832
|
+
* required means a NEW renderer that forgets to pass it fails to compile.
|
|
1833
|
+
*/
|
|
1834
|
+
deviceState: DeviceState;
|
|
1835
|
+
/**
|
|
1836
|
+
* The live conversational SESSION, gated to plugins that declared
|
|
1837
|
+
* `capabilities: { voice: true }`. A SESSION, not a device sensor: start/stop
|
|
1838
|
+
* the loop, read + subscribe to the transcript, and speak first via
|
|
1839
|
+
* `prompt()`. The mic-active indicator is platform-owned and cannot be
|
|
1840
|
+
* suppressed from here.
|
|
1841
|
+
*
|
|
1842
|
+
* REQUIRED, like `deviceState`: every renderer builds it through
|
|
1843
|
+
* resolvePluginRuntimeProps, so the object is always present. An undeclared
|
|
1844
|
+
* plugin gets a refusal (reads say so, actions throw) — never a fake `idle`.
|
|
1845
|
+
*/
|
|
1846
|
+
voice: VoiceAPI;
|
|
309
1847
|
theme: ThemeTokens;
|
|
310
1848
|
context: PluginContext;
|
|
311
1849
|
/** Engine-supplied props from `sys.focusUiPlugin(pluginId, props)`.
|
|
@@ -349,6 +1887,11 @@ interface PluginManifest$1 {
|
|
|
349
1887
|
actions: string[];
|
|
350
1888
|
};
|
|
351
1889
|
capabilities?: Record<string, unknown>;
|
|
1890
|
+
/** L3 DeviceState domains this plugin reads (props.deviceState), as Core passes
|
|
1891
|
+
* them through from the solution's declaration. Absent → NO domains (fail
|
|
1892
|
+
* closed): the loaded bundle's own `stateDomains` is never consulted when a
|
|
1893
|
+
* manifest exists. Read via resolvePluginRuntimeProps. */
|
|
1894
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
352
1895
|
_source?: string;
|
|
353
1896
|
_connectorId?: string;
|
|
354
1897
|
_mcpEndpoint?: string;
|
|
@@ -357,7 +1900,13 @@ interface RegisteredUIPlugin {
|
|
|
357
1900
|
id: string;
|
|
358
1901
|
type: 'ui' | 'hybrid';
|
|
359
1902
|
capabilities?: NativeCapabilities$1;
|
|
1903
|
+
/** L3 DeviceState domains this plugin's UI reads (props.deviceState). The
|
|
1904
|
+
* trusted source for a HOST-registered plugin (no server manifest), read by
|
|
1905
|
+
* resolvePluginRuntimeProps. Carried by register(), same as registerTools. */
|
|
1906
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
360
1907
|
Component: React.FC<PluginProps>;
|
|
1908
|
+
/** Optional `tools/` section — a plugin may ship both UI and local tools. */
|
|
1909
|
+
tools?: DeviceToolDefinition[];
|
|
361
1910
|
}
|
|
362
1911
|
interface RegisteredServicePlugin {
|
|
363
1912
|
id: string;
|
|
@@ -367,7 +1916,50 @@ interface RegisteredServicePlugin {
|
|
|
367
1916
|
onStart: (props: Omit<PluginProps, 'theme'>) => Promise<void> | void;
|
|
368
1917
|
onStop?: () => Promise<void> | void;
|
|
369
1918
|
}
|
|
370
|
-
|
|
1919
|
+
/** What a device tool implementation is handed at call time. */
|
|
1920
|
+
interface DeviceToolContext {
|
|
1921
|
+
bridge: PluginBridge;
|
|
1922
|
+
/** Device Bridge — camera, location, sensors, storage. */
|
|
1923
|
+
native: PluginNative;
|
|
1924
|
+
/**
|
|
1925
|
+
* L3 semantic DeviceState (visual/spatial/activity/…), gated to the domains
|
|
1926
|
+
* the plugin declared in `stateDomains`. Every domain is always present; an
|
|
1927
|
+
* undeclared one answers with an explicit "not declared" rather than data —
|
|
1928
|
+
* the same gating a UI plugin's `props.deviceState` gets. This is what lets an
|
|
1929
|
+
* EXTERNAL tool (e.g. Ada Guide's `guide.observe`) read `ctx.deviceState.visual`
|
|
1930
|
+
* over the public surface, with no SDK-internal access.
|
|
1931
|
+
*
|
|
1932
|
+
* `subscribe` refuses (RESTRICTED) on every domain: a call has no lifetime to
|
|
1933
|
+
* own a subscription, and the engines outlive it. Read instead.
|
|
1934
|
+
*/
|
|
1935
|
+
deviceState: DeviceState;
|
|
1936
|
+
/** Cooperative cancellation: caller cancelled, or the session went away. */
|
|
1937
|
+
signal?: AbortSignal;
|
|
1938
|
+
}
|
|
1939
|
+
type DeviceToolFn = (args: any, ctx: DeviceToolContext) => Promise<any> | any;
|
|
1940
|
+
interface DeviceToolDefinition {
|
|
1941
|
+
/** Domain name, as the catalog and the caller see it. e.g. "guide.observe" */
|
|
1942
|
+
name: string;
|
|
1943
|
+
description?: string;
|
|
1944
|
+
/** JSON Schema for arguments — advertised to Core, not enforced here. */
|
|
1945
|
+
input_schema?: Record<string, unknown>;
|
|
1946
|
+
run: DeviceToolFn;
|
|
1947
|
+
}
|
|
1948
|
+
/** A plugin that ships tools and no UI at all. */
|
|
1949
|
+
interface RegisteredToolsPlugin {
|
|
1950
|
+
id: string;
|
|
1951
|
+
type: 'tools';
|
|
1952
|
+
capabilities?: NativeCapabilities$1;
|
|
1953
|
+
/**
|
|
1954
|
+
* L3 DeviceState domains this plugin's tools read via `ctx.deviceState` (e.g.
|
|
1955
|
+
* `['visual']`). For a remote bundle this is the author's statement, not a
|
|
1956
|
+
* grant: the tools get the manifest's `stateDomains` (resolvePluginGrants),
|
|
1957
|
+
* and a domain declared here but not there is denied with a warning.
|
|
1958
|
+
*/
|
|
1959
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
1960
|
+
tools: DeviceToolDefinition[];
|
|
1961
|
+
}
|
|
1962
|
+
type RegisteredPlugin = RegisteredUIPlugin | RegisteredServicePlugin | RegisteredToolsPlugin;
|
|
371
1963
|
|
|
372
1964
|
/**
|
|
373
1965
|
* Plugin registration API.
|
|
@@ -376,12 +1968,35 @@ type RegisteredPlugin = RegisteredUIPlugin | RegisteredServicePlugin;
|
|
|
376
1968
|
|
|
377
1969
|
/**
|
|
378
1970
|
* Register a UI or hybrid plugin (has a visual component).
|
|
1971
|
+
*
|
|
1972
|
+
* `tools` is optional: a plugin may ship UI and local tools together. Tools are
|
|
1973
|
+
* picked up when the bundle loads, independently of whether the UI is mounted.
|
|
379
1974
|
*/
|
|
380
1975
|
declare function register(id: string, config: {
|
|
381
1976
|
type?: 'ui' | 'hybrid';
|
|
382
1977
|
capabilities?: NativeCapabilities$1;
|
|
1978
|
+
/** L3 DeviceState domains this plugin's UI reads via props.deviceState (e.g.
|
|
1979
|
+
* ['spatial','visual']). Carried through so a HOST-registered plugin (no
|
|
1980
|
+
* server manifest) can declare its domains — resolvePluginRuntimeProps reads
|
|
1981
|
+
* this as the trusted source when there is no manifest. Dropping it here left
|
|
1982
|
+
* every registered UI plugin with no L3, silently. Mirrors registerTools. */
|
|
1983
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
383
1984
|
Component: React.FC<PluginProps>;
|
|
1985
|
+
tools?: DeviceToolDefinition[];
|
|
384
1986
|
}): RegisteredUIPlugin;
|
|
1987
|
+
/**
|
|
1988
|
+
* Register a tools-only plugin — a `tools/` section with no UI at all.
|
|
1989
|
+
*
|
|
1990
|
+
* Nothing here mounts. The tools become callable as soon as the bundle is
|
|
1991
|
+
* evaluated, and stay callable with nothing of the plugin on screen.
|
|
1992
|
+
*/
|
|
1993
|
+
declare function registerTools(id: string, config: {
|
|
1994
|
+
capabilities?: NativeCapabilities$1;
|
|
1995
|
+
/** L3 DeviceState domains the tools read via ctx.deviceState (e.g. ['visual']).
|
|
1996
|
+
* For a remote bundle the MANIFEST grants them — see RegisteredToolsPlugin. */
|
|
1997
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
1998
|
+
tools: DeviceToolDefinition[];
|
|
1999
|
+
}): RegisteredToolsPlugin;
|
|
385
2000
|
/**
|
|
386
2001
|
* Register a headless service plugin (no UI).
|
|
387
2002
|
*/
|
|
@@ -460,6 +2075,20 @@ interface ActionButtonProps {
|
|
|
460
2075
|
}
|
|
461
2076
|
declare function ActionButton({ theme, label, icon, onPress, style }: ActionButtonProps): React$1.FunctionComponentElement<react_native.PressableProps & React$1.RefAttributes<View>>;
|
|
462
2077
|
|
|
2078
|
+
interface CameraPreviewProps {
|
|
2079
|
+
/**
|
|
2080
|
+
* The plugin's own `props.native`. REQUIRED, and it is the gate: the runtime
|
|
2081
|
+
* stamps the declared capabilities onto this object, so passing it is how the
|
|
2082
|
+
* component knows whether this plugin may show a camera at all.
|
|
2083
|
+
*/
|
|
2084
|
+
native: PluginNative;
|
|
2085
|
+
facing?: 'front' | 'back';
|
|
2086
|
+
style?: any;
|
|
2087
|
+
/** Rendered instead of the built-in panel when the preview is unavailable. */
|
|
2088
|
+
renderUnavailable?: (reason: string) => React$1.ReactElement | null;
|
|
2089
|
+
}
|
|
2090
|
+
declare function CameraPreview({ native, facing, style, renderUnavailable }: CameraPreviewProps): react_jsx_runtime.JSX.Element | null;
|
|
2091
|
+
|
|
463
2092
|
type ThemeColors = ThemeTokens['colors'];
|
|
464
2093
|
/** Override the plugin theme (host brand). Call once at startup — mounted
|
|
465
2094
|
* plugins re-render with the new tokens. */
|
|
@@ -486,8 +2115,88 @@ declare function useApi(bridge: PluginBridge): PluginApi;
|
|
|
486
2115
|
declare const PluginSDK: {
|
|
487
2116
|
readonly register: typeof register;
|
|
488
2117
|
readonly registerService: typeof registerService;
|
|
2118
|
+
readonly registerTools: typeof registerTools;
|
|
489
2119
|
};
|
|
490
2120
|
|
|
2121
|
+
/**
|
|
2122
|
+
* Host DI for the SEMANTIC layer — the `props.deviceState` counterpart of
|
|
2123
|
+
* `setPluginNativeProvider`.
|
|
2124
|
+
*
|
|
2125
|
+
* Kept in its own file rather than added to host.ts, for the same reason the
|
|
2126
|
+
* two layers have separate namespaces: the moment they share a module, the
|
|
2127
|
+
* next person adds a domain to `PluginNative` or a capability to
|
|
2128
|
+
* `stateDomains` and the boundary starts to blur.
|
|
2129
|
+
*
|
|
2130
|
+
* THE GATE IS NOT THE MECHANICAL ONE. `gatePluginNative` replaces an
|
|
2131
|
+
* undeclared capability with a proxy whose methods warn and return `undefined`
|
|
2132
|
+
* — so `await native.location.getCurrent()` resolves to `undefined`, and
|
|
2133
|
+
* `pos?.latitude` is `undefined`, which flows into a model answer as a
|
|
2134
|
+
* confident nothing. A silent undefined is a fabrication vector.
|
|
2135
|
+
*
|
|
2136
|
+
* Here, an undeclared domain returns a real snapshot in which everything is
|
|
2137
|
+
* explicitly `unknown` / `null`, with `freshness.state = 'unknown'`. The
|
|
2138
|
+
* plugin gets a well-formed answer that says "I know nothing", which no
|
|
2139
|
+
* reasonable code path can mistake for data.
|
|
2140
|
+
*/
|
|
2141
|
+
|
|
2142
|
+
/** Host DI: the app supplies the engines it has. Called once at startup. */
|
|
2143
|
+
declare function setDeviceStateProvider(state: Partial<DeviceState>): void;
|
|
2144
|
+
/**
|
|
2145
|
+
* The `deviceState` a plugin receives, gated by what it declared.
|
|
2146
|
+
*
|
|
2147
|
+
* Every domain is always present — a plugin can call any of the eight without
|
|
2148
|
+
* an existence check. What differs is whether it answers with data, or with
|
|
2149
|
+
* an explicit "I know nothing" and a reason.
|
|
2150
|
+
*/
|
|
2151
|
+
declare function gateDeviceState(declared?: readonly DeviceBridgeDomain[]): DeviceState;
|
|
2152
|
+
/** Which domains this build can actually answer — for diagnostics. */
|
|
2153
|
+
declare function implementedStateDomains(): DeviceBridgeDomain[];
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Host DI + gate for `props.voice` — the session counterpart of
|
|
2157
|
+
* setDeviceStateProvider / setPluginNativeProvider.
|
|
2158
|
+
*
|
|
2159
|
+
* The app mounts one live voice session (VoiceSessionHost in @ateam-ai-mobile/ui)
|
|
2160
|
+
* and hands it here once at startup via setVoiceProvider. gateVoice then decides,
|
|
2161
|
+
* per plugin, whether that session is exposed:
|
|
2162
|
+
*
|
|
2163
|
+
* - declared (`capabilities: { voice: true }`) + a provider present → the real
|
|
2164
|
+
* session.
|
|
2165
|
+
* - declared but no provider (no session host in this build) → a refusal that
|
|
2166
|
+
* names the reason.
|
|
2167
|
+
* - NOT declared → a refusal that names the reason.
|
|
2168
|
+
*
|
|
2169
|
+
* ABSENCE IS AN ANSWER (spec invariant 6). A denied surface never fakes an
|
|
2170
|
+
* `idle` snapshot and never silently no-ops: reads report `state: 'error'` with
|
|
2171
|
+
* an explicit `lastError`, and actions throw. A silent `idle` would read to a
|
|
2172
|
+
* reasoning loop as "a working session that simply isn't connected yet" — a
|
|
2173
|
+
* fabrication, the same class of bug as an undefined that flows into an answer.
|
|
2174
|
+
*/
|
|
2175
|
+
|
|
2176
|
+
/** Host DI: the app supplies its live (owner-aware) voice session. Called once
|
|
2177
|
+
* at startup; pass null to detach (e.g. in a build with no voice). */
|
|
2178
|
+
declare function setVoiceProvider(provider: VoiceSessionProvider | null): void;
|
|
2179
|
+
/** Whether this build has a voice session to expose — for diagnostics. */
|
|
2180
|
+
declare function implementedVoice(): boolean;
|
|
2181
|
+
/**
|
|
2182
|
+
* The `voice` a plugin receives, gated by whether it declared the capability AND
|
|
2183
|
+
* scoped to its identity.
|
|
2184
|
+
*
|
|
2185
|
+
* `declared` is `!!capabilities.voice` from the plugin's TRUSTED declaration
|
|
2186
|
+
* (resolved in pluginRuntime.ts — the server manifest for a remote plugin, the
|
|
2187
|
+
* registry entry for a host-owned one). A downloaded bundle cannot grant itself
|
|
2188
|
+
* voice. `pluginId` is the trusted identity the returned facade is bound to.
|
|
2189
|
+
*
|
|
2190
|
+
* Reading the transcript of a live session is a privacy surface, not just an
|
|
2191
|
+
* API: this gate is the only thing between a plugin and everything the user
|
|
2192
|
+
* says. Two protections stack: (1) it is coupled to evidence — a live session
|
|
2193
|
+
* shows the platform-owned, unsuppressable mic indicator, so there is no path to
|
|
2194
|
+
* a transcript without visible proof the mic is/was live; and (2) it is scoped
|
|
2195
|
+
* to the owner, so declaring `voice` lets a plugin run ITS OWN conversation, not
|
|
2196
|
+
* eavesdrop on another solution's. A missing identity FAILS CLOSED.
|
|
2197
|
+
*/
|
|
2198
|
+
declare function gateVoice(declared: boolean, pluginId: string): VoiceAPI;
|
|
2199
|
+
|
|
491
2200
|
/**
|
|
492
2201
|
* Host integration points — the DI seams that keep this package free of
|
|
493
2202
|
* Expo imports. The host app (or @ateam-ai-mobile/expo) plugs in concrete providers
|
|
@@ -500,7 +2209,8 @@ declare function setHapticsProvider(haptics: HapticsAPI | null): void;
|
|
|
500
2209
|
/** No-op safe accessor — surfaces call this for tap feedback. */
|
|
501
2210
|
declare function getHaptics(): HapticsAPI | null;
|
|
502
2211
|
/** Install the device-capabilities implementation plugins receive as
|
|
503
|
-
* `props.native
|
|
2212
|
+
* `props.device` (the L1 native modules keep the `native` name; the plugin-
|
|
2213
|
+
* facing prop is `device`). The host owns the native modules. */
|
|
504
2214
|
declare function setPluginNativeProvider(native: PluginNative): void;
|
|
505
2215
|
declare function getPluginNative(): PluginNative;
|
|
506
2216
|
/**
|
|
@@ -512,7 +2222,18 @@ declare function getPluginNative(): PluginNative;
|
|
|
512
2222
|
* Flag mapping mirrors the manifest contract: `storage` is gated by
|
|
513
2223
|
* `offline` (offline data ⇒ local storage).
|
|
514
2224
|
*/
|
|
515
|
-
declare function gatePluginNative(native: PluginNative, declared?: NativeCapabilities$1): PluginNative;
|
|
2225
|
+
declare function gatePluginNative(native: PluginNative, declared?: NativeCapabilities$1, pluginId?: string): PluginNative;
|
|
2226
|
+
/** Props the host's preview implementation receives. */
|
|
2227
|
+
interface CameraPreviewImplProps {
|
|
2228
|
+
facing?: 'front' | 'back';
|
|
2229
|
+
style?: any;
|
|
2230
|
+
renderUnavailable?: (reason: string) => any;
|
|
2231
|
+
}
|
|
2232
|
+
/** Install the host's camera preview (e.g. an expo-camera CameraView). */
|
|
2233
|
+
declare function setCameraPreviewProvider(Component: ((props: CameraPreviewImplProps) => any) | null): void;
|
|
2234
|
+
/** Null when the host installed none — the SDK then renders a stated reason,
|
|
2235
|
+
* never a black rectangle that looks like a dark room. */
|
|
2236
|
+
declare function getCameraPreviewProvider(): ((props: CameraPreviewImplProps) => any) | null;
|
|
516
2237
|
type PluginResolver = RegisteredPlugin | (() => Promise<RegisteredPlugin | null>);
|
|
517
2238
|
declare function registerNativePlugin(id: string, plugin: PluginResolver): void;
|
|
518
2239
|
declare function unregisterNativePlugin(id: string): void;
|
|
@@ -522,11 +2243,262 @@ declare const nativePluginRegistry: {
|
|
|
522
2243
|
};
|
|
523
2244
|
declare function registerBundleModules(modules: Record<string, any>): void;
|
|
524
2245
|
|
|
2246
|
+
/** The gated surface a plugin Component receives. `device` and `native` are the
|
|
2247
|
+
* SAME object (native is the deprecated alias). */
|
|
2248
|
+
interface PluginRuntimeProps {
|
|
2249
|
+
device: PluginNative;
|
|
2250
|
+
native: PluginNative;
|
|
2251
|
+
deviceState: DeviceState;
|
|
2252
|
+
voice: VoiceAPI;
|
|
2253
|
+
}
|
|
2254
|
+
/** One declaration source — either the server manifest or the registry entry. */
|
|
2255
|
+
interface PluginDeclaration {
|
|
2256
|
+
capabilities?: NativeCapabilities$1 | Record<string, unknown>;
|
|
2257
|
+
native?: NativeCapabilities$1;
|
|
2258
|
+
stateDomains?: readonly DeviceBridgeDomain[];
|
|
2259
|
+
}
|
|
2260
|
+
interface PluginRuntimeSource {
|
|
2261
|
+
pluginId: string;
|
|
2262
|
+
/**
|
|
2263
|
+
* The server manifest (Core-passed) — the TRUSTED authority for a REMOTE
|
|
2264
|
+
* plugin. `capabilities` is the current field Core emits; `native` is the
|
|
2265
|
+
* legacy one. A grant absent here is NOT GRANTED (fail closed).
|
|
2266
|
+
*/
|
|
2267
|
+
manifest?: PluginDeclaration | null;
|
|
2268
|
+
/**
|
|
2269
|
+
* The registry entry — the trusted authority for a HOST-OWNED plugin
|
|
2270
|
+
* (registered locally, no server manifest). For a REMOTE plugin this is the
|
|
2271
|
+
* downloaded bundle's own claim and is UNTRUSTED for gating; it is used ONLY
|
|
2272
|
+
* when there is no manifest.
|
|
2273
|
+
*/
|
|
2274
|
+
plugin?: PluginDeclaration | null;
|
|
2275
|
+
}
|
|
2276
|
+
/** What a plugin is GRANTED — resolved from its one trusted source. The same
|
|
2277
|
+
* value gates its rendered surface and its device tools (the tool registry
|
|
2278
|
+
* receives it through bundleManager.load/loadTools). */
|
|
2279
|
+
interface PluginGrants {
|
|
2280
|
+
capabilities?: NativeCapabilities$1;
|
|
2281
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Resolve a plugin's grants — the SINGLE place this happens, for every rendered
|
|
2285
|
+
* surface AND for the device tools its bundle registers, so a plugin gets
|
|
2286
|
+
* identical access through either.
|
|
2287
|
+
*
|
|
2288
|
+
* TRUST BOUNDARY. Exactly ONE source is authoritative: the server `manifest` for
|
|
2289
|
+
* a remote plugin, the registry `plugin` for a host-owned one. They are NEVER
|
|
2290
|
+
* mixed, and a remote plugin's own downloaded bundle claim NEVER expands its
|
|
2291
|
+
* capabilities or L3 domains past what the trusted source granted. If the
|
|
2292
|
+
* trusted source omits a grant, it is not granted — fail closed. (If Core does
|
|
2293
|
+
* not carry `stateDomains` for a remote plugin, the fix is Core passing it, not
|
|
2294
|
+
* trusting the bundle: Core already holds the solution's declaration.)
|
|
2295
|
+
*/
|
|
2296
|
+
declare function resolvePluginGrants(src: PluginRuntimeSource): PluginGrants;
|
|
2297
|
+
/** Turn a plugin's grants into the gated { device, native, deviceState, voice } it
|
|
2298
|
+
* receives on every rendered surface. */
|
|
2299
|
+
declare function resolvePluginRuntimeProps(src: PluginRuntimeSource): PluginRuntimeProps;
|
|
2300
|
+
|
|
2301
|
+
interface DeviceToolSummary {
|
|
2302
|
+
name: string;
|
|
2303
|
+
pluginId: string;
|
|
2304
|
+
description?: string;
|
|
2305
|
+
input_schema?: Record<string, unknown>;
|
|
2306
|
+
}
|
|
2307
|
+
type DeviceToolOutcome = {
|
|
2308
|
+
ok: true;
|
|
2309
|
+
result: any;
|
|
2310
|
+
} | {
|
|
2311
|
+
ok: false;
|
|
2312
|
+
error: string;
|
|
2313
|
+
reason: 'not_found' | 'no_context' | 'timeout' | 'failed';
|
|
2314
|
+
};
|
|
2315
|
+
/**
|
|
2316
|
+
* Override how a device tool reaches the Device Bridge. Rarely needed — the
|
|
2317
|
+
* default below builds the same bridge/native pair a plugin gets when it
|
|
2318
|
+
* renders, gated by the capabilities the plugin declared.
|
|
2319
|
+
*/
|
|
2320
|
+
declare function setDeviceToolContextProvider(fn: (pluginId: string, capabilities?: NativeCapabilities$1, stateDomains?: DeviceBridgeDomain[]) => DeviceToolContext): void;
|
|
2321
|
+
/**
|
|
2322
|
+
* Register a plugin's tools. Called from the bundle pipeline at load time.
|
|
2323
|
+
*
|
|
2324
|
+
* A name already taken by ANOTHER plugin is refused — a bundle loaded later
|
|
2325
|
+
* must not be able to take over "guide.observe" from the plugin that owns it.
|
|
2326
|
+
* Re-registering the same plugin (bundle reload) replaces its own entries.
|
|
2327
|
+
*/
|
|
2328
|
+
declare function registerDeviceTools(pluginId: string, tools: DeviceToolDefinition[], capabilities?: NativeCapabilities$1, stateDomains?: DeviceBridgeDomain[]): string[];
|
|
2329
|
+
/** Drop every tool owned by a plugin (bundle eviction / plugin revoked). */
|
|
2330
|
+
declare function unregisterDeviceTools(pluginId: string): void;
|
|
2331
|
+
/** Is `name` registered — and, given `pluginId`, registered BY that plugin?
|
|
2332
|
+
* (A clean voice call only lets its owner's own tools run.) */
|
|
2333
|
+
declare function hasDeviceTool(name: string, pluginId?: string): boolean;
|
|
2334
|
+
declare function listDeviceTools(): DeviceToolSummary[];
|
|
2335
|
+
/**
|
|
2336
|
+
* Invoke a device tool. Never throws — every failure comes back as a typed
|
|
2337
|
+
* outcome, because the caller is usually a reasoning loop that has to say
|
|
2338
|
+
* something useful out loud rather than swallow an exception.
|
|
2339
|
+
*/
|
|
2340
|
+
declare function callDeviceTool(name: string, args?: any, opts?: {
|
|
2341
|
+
timeoutMs?: number;
|
|
2342
|
+
signal?: AbortSignal;
|
|
2343
|
+
}): Promise<DeviceToolOutcome>;
|
|
2344
|
+
/** Test/host reset. */
|
|
2345
|
+
declare function clearDeviceTools(): void;
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Load every plugin that declares `capabilities.device_tools`, so its tools are
|
|
2349
|
+
* registered and callable. Best-effort and idempotent: safe to call on every
|
|
2350
|
+
* call start. Returns the tool names currently registered.
|
|
2351
|
+
*
|
|
2352
|
+
* Pass `force` after a plugin catalog change (a newly deployed connector).
|
|
2353
|
+
*/
|
|
2354
|
+
declare function ensureDeviceToolsLoaded({ force }?: {
|
|
2355
|
+
force?: boolean | undefined;
|
|
2356
|
+
}): Promise<string[]>;
|
|
2357
|
+
/** Forget that we loaded, so the next call re-reads the catalog. */
|
|
2358
|
+
declare function resetDeviceToolLoader(): void;
|
|
2359
|
+
|
|
2360
|
+
declare function registerPlatformDeviceTools(): void;
|
|
2361
|
+
|
|
2362
|
+
interface SessionSnapshot {
|
|
2363
|
+
authenticated: boolean;
|
|
2364
|
+
/** The tenant the session is scoped to (derive it; never a stored copy). */
|
|
2365
|
+
tenant: string | null;
|
|
2366
|
+
}
|
|
2367
|
+
type DeviceToolsAction = 'load' | 'reset' | 'none';
|
|
2368
|
+
/**
|
|
2369
|
+
* PURE: what should the device-tool registry do for this session transition?
|
|
2370
|
+
* - a new authenticated tenant (initial sign-in OR a switch) → 'load'
|
|
2371
|
+
* - the same authenticated tenant again (e.g. a silent token refresh) → 'none'
|
|
2372
|
+
* - leaving authenticated (sign-out) → 'reset'
|
|
2373
|
+
*/
|
|
2374
|
+
declare function deviceToolsActionForSession(next: SessionSnapshot, prevTenant: string | null): {
|
|
2375
|
+
action: DeviceToolsAction;
|
|
2376
|
+
tenant: string | null;
|
|
2377
|
+
};
|
|
2378
|
+
/** The effects a session transition drives — injected so a test can fake them. */
|
|
2379
|
+
interface DeviceToolsSessionDeps {
|
|
2380
|
+
/** Drop the previous tenant's device tools (a switch does not run logout teardown). */
|
|
2381
|
+
clear: () => void;
|
|
2382
|
+
/** Ensure the static, host-owned platform device tools are present. */
|
|
2383
|
+
registerPlatform: () => void;
|
|
2384
|
+
/** Forget the "already loaded" latch so a later (non-forced) load runs — sign-out. */
|
|
2385
|
+
resetLoader: () => void;
|
|
2386
|
+
/** Fire-and-forget reload of the current tenant's external device-tool bundles. */
|
|
2387
|
+
load: () => void;
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Apply a session transition to the device-tool registry. Call from the host's
|
|
2391
|
+
* existing auth-session subscription with a derived snapshot. Idempotent per
|
|
2392
|
+
* tenant: repeated snapshots for the same authenticated tenant do nothing, so a
|
|
2393
|
+
* silent token refresh does not churn the registry. Returns the action taken.
|
|
2394
|
+
*/
|
|
2395
|
+
declare function syncDeviceToolsForSession(next: SessionSnapshot, deps?: DeviceToolsSessionDeps): DeviceToolsAction;
|
|
2396
|
+
/** Test seam: forget the last-seen tenant so a test starts from signed-out. */
|
|
2397
|
+
declare function __resetSessionSyncForTest(): void;
|
|
2398
|
+
|
|
2399
|
+
type PostResult = (correlationId: string, result: any, error?: string) => Promise<void>;
|
|
2400
|
+
/**
|
|
2401
|
+
* Host DI: how to answer the server. `@ateam-ai-mobile/core` supplies
|
|
2402
|
+
* postCommandResult; kept injectable so this package stays free of the REST
|
|
2403
|
+
* client and so tests can observe the answer.
|
|
2404
|
+
*/
|
|
2405
|
+
declare function setDeviceToolResultPoster(fn: PostResult): void;
|
|
2406
|
+
/** Is this stream command a device tool call? */
|
|
2407
|
+
declare function isDeviceToolCommand(cmd: any): boolean;
|
|
2408
|
+
/**
|
|
2409
|
+
* Run a server-dispatched device tool and answer.
|
|
2410
|
+
*
|
|
2411
|
+
* Never throws — the server is blocked on the correlationId, so the one thing
|
|
2412
|
+
* we must not do is stay silent. Every path posts something back.
|
|
2413
|
+
*/
|
|
2414
|
+
declare function handleServerDeviceToolCall(cmd: any): Promise<void>;
|
|
2415
|
+
|
|
2416
|
+
/** Where the bytes currently running came from. */
|
|
2417
|
+
type LoadedBundleSource = 'download' | 'disk-cache' | 'memory';
|
|
2418
|
+
/**
|
|
2419
|
+
* Provenance of one loaded bundle — what is RUNNING, not what was requested.
|
|
2420
|
+
*
|
|
2421
|
+
* `hash` is the sha256 of the evaluated bytes. `expected` is the hash the
|
|
2422
|
+
* server's manifest advertised for that url. Equal means the phone is running
|
|
2423
|
+
* the build the server currently describes; different means it is not, and the
|
|
2424
|
+
* pair says so without needing a rebuild, a reinstall or a guess.
|
|
2425
|
+
*/
|
|
2426
|
+
interface LoadedBundleInfo {
|
|
2427
|
+
bundleUrl: string;
|
|
2428
|
+
version: string;
|
|
2429
|
+
/** sha256 (hex, lowercase) of the bytes that were evaluated. */
|
|
2430
|
+
hash: string | null;
|
|
2431
|
+
/** sha256 the server advertised for this url at load time, if any. */
|
|
2432
|
+
expected: string | null;
|
|
2433
|
+
source: LoadedBundleSource;
|
|
2434
|
+
tenant: string | null;
|
|
2435
|
+
loadedAt: number;
|
|
2436
|
+
}
|
|
525
2437
|
declare class BundleManager {
|
|
526
|
-
/** In-memory module cache: bundleUrl → evaluated module.exports
|
|
2438
|
+
/** In-memory module cache: bundleUrl → evaluated module.exports. Holds ONE
|
|
2439
|
+
* tenant's modules at a time — the tenant boundary is `cacheTenant` /
|
|
2440
|
+
* `requireTenant()`, NOT the key. */
|
|
527
2441
|
private modules;
|
|
2442
|
+
/** What is actually RUNNING, keyed by bundleUrl (for the current tenant). See
|
|
2443
|
+
* getLoadedBundles(). */
|
|
2444
|
+
private provenance;
|
|
528
2445
|
/** In-flight download deduplication: bundleUrl → Promise */
|
|
529
2446
|
private inflight;
|
|
2447
|
+
/**
|
|
2448
|
+
* The tenant the in-memory caches (modules / provenance / inflight) currently
|
|
2449
|
+
* belong to. This is the tenant BOUNDARY for evaluated code.
|
|
2450
|
+
*
|
|
2451
|
+
* A bundleUrl resolves PER-TENANT on the server — one url, different bytes for
|
|
2452
|
+
* different tenants — so a memory hit keyed on url alone would hand tenant B
|
|
2453
|
+
* the module tenant A already evaluated and ran. PR #9 scoped the DISK cache
|
|
2454
|
+
* (per-tenant index + hash gate); the in-memory map was left url-keyed. This
|
|
2455
|
+
* closes that: the caches only ever hold ONE tenant's modules, and a switch
|
|
2456
|
+
* drops them before anything is served.
|
|
2457
|
+
*/
|
|
2458
|
+
private cacheTenant;
|
|
2459
|
+
/**
|
|
2460
|
+
* pluginIds this manager registered device tools for. A tenant switch drops
|
|
2461
|
+
* the in-memory modules AND unregisters these, so the incoming tenant's bundle
|
|
2462
|
+
* re-registers into a clean registry instead of being refused as a duplicate
|
|
2463
|
+
* of the previous tenant's tools.
|
|
2464
|
+
*/
|
|
2465
|
+
private registeredToolPluginIds;
|
|
2466
|
+
/** bundleUrl → the trusted declaration last recorded on its disk-cache entry
|
|
2467
|
+
* this session, so a memory hit with unchanged grants skips the index I/O. */
|
|
2468
|
+
private recordedDeclarations;
|
|
2469
|
+
/**
|
|
2470
|
+
* Enforce the in-memory tenant boundary. Call at the top of every public load
|
|
2471
|
+
* path, BEFORE any cache read.
|
|
2472
|
+
*
|
|
2473
|
+
* FAIL CLOSED: without a tenant identity we cannot prove a cached module is
|
|
2474
|
+
* this session's, so we drop everything and refuse rather than risk serving
|
|
2475
|
+
* one tenant's evaluated code to another. On a tenant change we drop the
|
|
2476
|
+
* previous tenant's modules (and their tool registrations) before proceeding.
|
|
2477
|
+
*/
|
|
2478
|
+
/**
|
|
2479
|
+
* Monotonic load generation. Bumped on every in-memory wipe (a tenant change,
|
|
2480
|
+
* fail-closed, or clearAll). A load captures it at ENTRY; a completion whose
|
|
2481
|
+
* generation no longer matches is stale and is discarded before it can commit.
|
|
2482
|
+
* This closes the async race the sequential guard cannot: tenant A's in-flight
|
|
2483
|
+
* loadModule can resolve AFTER a switch to B — clearing `inflight` does not
|
|
2484
|
+
* cancel that Promise — and would otherwise write A's module / provenance /
|
|
2485
|
+
* device tools into the singleton now marked cacheTenant=B.
|
|
2486
|
+
*/
|
|
2487
|
+
private epoch;
|
|
2488
|
+
private requireTenant;
|
|
2489
|
+
/** Drop every in-memory cache and unregister the device tools this manager
|
|
2490
|
+
* registered — used on a tenant change and by clearAll(). */
|
|
2491
|
+
private dropInMemory;
|
|
2492
|
+
/** True once the boundary has moved since a load captured `epoch` — i.e. a
|
|
2493
|
+
* tenant switch (or wipe) happened while this load was in flight. Its results
|
|
2494
|
+
* belong to a tenant that is no longer current and must be discarded. */
|
|
2495
|
+
private isStale;
|
|
2496
|
+
/** Record a memory serve: the running bytes are the already-evaluated module,
|
|
2497
|
+
* and `expected` is refreshed from the manifest hash the caller JUST passed —
|
|
2498
|
+
* so after a redeploy the record reads hash !== expected (STALE) instead of
|
|
2499
|
+
* the old in-sync pair. The ONE place both memory paths (load()'s early return
|
|
2500
|
+
* and loadModule's) record reuse. No-op if nothing was recorded yet. */
|
|
2501
|
+
private markMemoryHit;
|
|
530
2502
|
/**
|
|
531
2503
|
* Load a plugin component. Returns the RegisteredUIPlugin object.
|
|
532
2504
|
* Handles: in-memory cache → disk cache → download → evaluate → extract.
|
|
@@ -535,8 +2507,26 @@ declare class BundleManager {
|
|
|
535
2507
|
* provided for any bundle the user has not previously verified — without
|
|
536
2508
|
* it, a tampered bundle is `eval`'d as JS with full RN runtime + every
|
|
537
2509
|
* native capability the manifest declares.
|
|
2510
|
+
*
|
|
2511
|
+
* `grants` is what the MANIFEST grants this plugin (resolvePluginGrants) — the
|
|
2512
|
+
* only authority its device tools are registered under. Omitted (e.g. a
|
|
2513
|
+
* prewarm from the slim plugin list) → the bundle is loaded but its tools are
|
|
2514
|
+
* NOT registered: without the manifest there is nothing trusted to gate them
|
|
2515
|
+
* on, and registering with nothing would downgrade a registration the
|
|
2516
|
+
* manifest-driven tool loader already made.
|
|
538
2517
|
*/
|
|
539
|
-
load(pluginId: string, bundleUrl: string, version: string, expectedHash?: string): Promise<RegisteredUIPlugin>;
|
|
2518
|
+
load(pluginId: string, bundleUrl: string, version: string, expectedHash?: string, grants?: PluginGrants): Promise<RegisteredUIPlugin>;
|
|
2519
|
+
/**
|
|
2520
|
+
* Load a bundle for its `tools/` section only — no UI required.
|
|
2521
|
+
*
|
|
2522
|
+
* Same download, same integrity check, same evaluation as `load()`; the only
|
|
2523
|
+
* difference is that a bundle exporting tools and no `Component` is a success
|
|
2524
|
+
* here rather than "no component found". Returns the tool names registered.
|
|
2525
|
+
*
|
|
2526
|
+
* `grants` is REQUIRED: the tools are registered under `pluginId` with exactly
|
|
2527
|
+
* what the manifest grants (see load()), never what the bundle claims.
|
|
2528
|
+
*/
|
|
2529
|
+
loadTools(pluginId: string, bundleUrl: string, version: string, expectedHash: string | undefined, grants: PluginGrants): Promise<string[]>;
|
|
540
2530
|
/**
|
|
541
2531
|
* Prewarm: load every plugin bundle in the background after app boot, so a
|
|
542
2532
|
* later open is instant (in-memory hit) instead of round-tripping the
|
|
@@ -548,6 +2538,8 @@ declare class BundleManager {
|
|
|
548
2538
|
bundleUrl: string;
|
|
549
2539
|
version: string;
|
|
550
2540
|
bundleHash?: string;
|
|
2541
|
+
/** The manifest's grants, when the caller has the manifest (see load()). */
|
|
2542
|
+
grants?: PluginGrants;
|
|
551
2543
|
}>): Promise<void>;
|
|
552
2544
|
/**
|
|
553
2545
|
* Clear all caches (in-memory + storage).
|
|
@@ -557,7 +2549,47 @@ declare class BundleManager {
|
|
|
557
2549
|
* Clear cache for a specific plugin/bundle.
|
|
558
2550
|
*/
|
|
559
2551
|
clearPlugin(pluginId: string): Promise<void>;
|
|
2552
|
+
/**
|
|
2553
|
+
* What every loaded bundle IS, right now: the sha256 of the bytes running,
|
|
2554
|
+
* the hash the server advertised, and where they came from.
|
|
2555
|
+
*
|
|
2556
|
+
* Deliberately not filtered or prettified — this is the raw answer to "did my
|
|
2557
|
+
* redeploy reach the phone?", and it is comparable directly against the
|
|
2558
|
+
* manifest `bundleHash` and the server's X-Bundle-SHA256 header.
|
|
2559
|
+
*/
|
|
2560
|
+
getLoadedBundles(): LoadedBundleInfo[];
|
|
560
2561
|
private loadModule;
|
|
2562
|
+
/**
|
|
2563
|
+
* Register THIS plugin's device tools under the TRUSTED declaration, then
|
|
2564
|
+
* record that declaration on the disk-cache entry. Runs on every trusted load
|
|
2565
|
+
* — memory hit included — so the grants in force are always the latest the
|
|
2566
|
+
* manifest gave (a revoked grant stops applying on the next load, not the next
|
|
2567
|
+
* app start). Callers run it only after their stale check.
|
|
2568
|
+
*/
|
|
2569
|
+
private commitTools;
|
|
2570
|
+
/**
|
|
2571
|
+
* Put the plugin's own `tools` section in the device tool registry — under the
|
|
2572
|
+
* pluginId and grants the MANIFEST resolved, never the bundle's own `id`,
|
|
2573
|
+
* `capabilities` or `stateDomains`. A bundle is code the manifest authorised
|
|
2574
|
+
* by hash; what it may touch is the manifest's to say, same as its rendered
|
|
2575
|
+
* surface (resolvePluginRuntimeProps). Its own `id` would also let it register
|
|
2576
|
+
* as another plugin — including the host's platform tools, which a same-id
|
|
2577
|
+
* re-registration replaces.
|
|
2578
|
+
*
|
|
2579
|
+
* Only this plugin's export (the same lookup the component uses) is
|
|
2580
|
+
* registered. Another export's tools in the same bundle belong to another
|
|
2581
|
+
* plugin and register when THAT plugin's manifest loads it.
|
|
2582
|
+
*/
|
|
2583
|
+
private registerToolsFromModule;
|
|
2584
|
+
/**
|
|
2585
|
+
* Record the trusted declaration on this tenant's disk-cache entry for the
|
|
2586
|
+
* bytes RUNNING — the recovery path (tools/loader loadFromDiskCache) has no
|
|
2587
|
+
* manifest and re-registers with exactly this. Only onto an entry this tenant
|
|
2588
|
+
* owns whose hash is the running hash: the grant was issued for these bytes,
|
|
2589
|
+
* and those are the bytes recovery will evaluate. Best-effort — a lost write
|
|
2590
|
+
* leaves no `trusted`, which recovery treats as fail closed.
|
|
2591
|
+
*/
|
|
2592
|
+
private recordTrustedDeclaration;
|
|
561
2593
|
/**
|
|
562
2594
|
* Download + integrity-verify a bundle. Refuses to return when:
|
|
563
2595
|
* - manifest hash and computed hash disagree
|
|
@@ -567,11 +2599,15 @@ declare class BundleManager {
|
|
|
567
2599
|
*/
|
|
568
2600
|
private download;
|
|
569
2601
|
private evaluate;
|
|
2602
|
+
/** Find a plugin component in a loaded module. */
|
|
2603
|
+
private getComponentFromMemory;
|
|
570
2604
|
/**
|
|
571
|
-
* Find
|
|
572
|
-
*
|
|
2605
|
+
* Find THIS plugin's export in a loaded module — the one lookup both the
|
|
2606
|
+
* component and the device tools use, so a plugin's UI and its tools always
|
|
2607
|
+
* come from the same export. `accept` says what the caller needs from it.
|
|
2608
|
+
* Handles: direct key match, PascalCase key, default export, multi-plugin .id match.
|
|
573
2609
|
*/
|
|
574
|
-
private
|
|
2610
|
+
private findPluginExport;
|
|
575
2611
|
private describeModule;
|
|
576
2612
|
}
|
|
577
2613
|
/** Singleton — shared across all plugin renderers */
|
|
@@ -584,6 +2620,7 @@ declare const bundleManager: BundleManager;
|
|
|
584
2620
|
* default is in-memory (bundles re-download per app session — correct,
|
|
585
2621
|
* just slower on cold start).
|
|
586
2622
|
*/
|
|
2623
|
+
|
|
587
2624
|
interface BundleCacheEntry {
|
|
588
2625
|
localPath: string;
|
|
589
2626
|
version: string;
|
|
@@ -591,6 +2628,37 @@ interface BundleCacheEntry {
|
|
|
591
2628
|
bundleUrl: string;
|
|
592
2629
|
/** SHA-256 (hex, lowercase) of the cached bundle bytes — integrity proof */
|
|
593
2630
|
bundleHash?: string;
|
|
2631
|
+
/**
|
|
2632
|
+
* Tenant whose session downloaded these bytes.
|
|
2633
|
+
*
|
|
2634
|
+
* A bundle URL is NOT tenant-independent: `/api/ui-plugins/<id>/bundle.js`
|
|
2635
|
+
* resolves per-tenant on the server, so the SAME url returns different code
|
|
2636
|
+
* for different tenants. On the normal load path the hash gate catches this
|
|
2637
|
+
* for free — the expected hash comes from the current tenant's manifest and
|
|
2638
|
+
* will not match another tenant's cached bytes. The disk-cache recovery path
|
|
2639
|
+
* has no manifest to check against, so it must discriminate by tenant.
|
|
2640
|
+
*
|
|
2641
|
+
* Absent on entries written before this field existed: unknown ownership, so
|
|
2642
|
+
* untrusted. Never read a missing tenant as "mine".
|
|
2643
|
+
*/
|
|
2644
|
+
tenant?: string;
|
|
2645
|
+
/**
|
|
2646
|
+
* The trusted declaration these bytes' device tools were last registered
|
|
2647
|
+
* under: the pluginId and grants resolved from the server manifest that named
|
|
2648
|
+
* this bundle (resolvePluginGrants). Recorded so the disk-cache recovery path —
|
|
2649
|
+
* which runs exactly when there is NO manifest — re-registers with the same
|
|
2650
|
+
* authority instead of the bundle's own claim.
|
|
2651
|
+
*
|
|
2652
|
+
* Absent → no trusted declaration is known for these bytes, so recovery skips
|
|
2653
|
+
* the entry (fail closed) and the catalog path registers it once the owning
|
|
2654
|
+
* connector answers again.
|
|
2655
|
+
*/
|
|
2656
|
+
trusted?: TrustedToolDeclaration;
|
|
2657
|
+
}
|
|
2658
|
+
/** Who a bundle's device tools belong to and what they are granted — always
|
|
2659
|
+
* from the manifest, never from the bundle. */
|
|
2660
|
+
interface TrustedToolDeclaration extends PluginGrants {
|
|
2661
|
+
pluginId: string;
|
|
594
2662
|
}
|
|
595
2663
|
interface BundleStorage {
|
|
596
2664
|
loadIndex(): Promise<Record<string, BundleCacheEntry>>;
|
|
@@ -821,6 +2889,9 @@ interface PluginManifestLike {
|
|
|
821
2889
|
name?: string;
|
|
822
2890
|
render?: any;
|
|
823
2891
|
_connectorId?: string;
|
|
2892
|
+
capabilities?: Record<string, unknown>;
|
|
2893
|
+
native?: NativeCapabilities$1;
|
|
2894
|
+
stateDomains?: DeviceBridgeDomain[];
|
|
824
2895
|
}
|
|
825
2896
|
interface UsePluginResult {
|
|
826
2897
|
plugin: RegisteredUIPlugin | null;
|
|
@@ -1139,4 +3210,4 @@ declare function isInlineSurface(m: SurfaceManifest): m is InlineSurfaceManifest
|
|
|
1139
3210
|
|
|
1140
3211
|
declare function SurfaceDispatcher(): react_jsx_runtime.JSX.Element;
|
|
1141
3212
|
|
|
1142
|
-
export { ActionButton, AdaOrb, type AdaOrbState, Badge, type BiometricsAPI, type BiometricsAPIv2, type Bridge, type BundleCacheEntry, type BundleStorage, type CameraAPI, Card, type ConnectivityAPI, type CookiesAPI, EmptyState, ErrorState, type FileSystemAPI, type HapticsAPI, type InlineSurfaceManifest, ListItem, LoadingState, type LocationAPI, MemoryBundleStorage, type MountSurfaceManifest, type NativeCapabilities$1 as NativeCapabilities, type NfcAPI, type NotificationsAPI, type PluginManifest as PlatformPluginManifest, type PluginBridge, type PluginContext, type PluginHooks$1 as PluginHooks, type PluginManifest$1 as PluginManifest, type PluginNative, type PluginProps, PluginSDK, type PluginSummary, type PluginSurfaceMeta, type RegisteredPlugin, type RegisteredServicePlugin, type RegisteredUIPlugin, type RemotePlugin, type RenderMode, type ScannerAPI, SearchBar, type SharingAPI, type StorageAPI, SurfaceCard, type SurfaceCardProps, SurfaceDispatcher, SurfaceDrawer, type SurfaceDrawerProps, SurfaceFullscreen, type SurfaceFullscreenProps, type SurfaceManifest, type SurfacePlacement, type SurfaceType, type SurfaceVisibility, TabBar, type ThemeTokens, bundleManager, createNativeBridge, gatePluginNative, getHaptics, getNativeBundleHash, getNativeBundleUrl, getNativeComponentId, getPlugin, getPluginNative, isInlineSurface, isMountSurface, listPlugins, nativePluginRegistry, register, registerBundleModules, registerNativePlugin, registerService, resolvePluginUrl, setBundleStorage, setHapticsProvider, setPluginNativeProvider, setPluginThemeColors, supportsNative, supportsWebView, unregisterNativePlugin, useApi, useNativeBridge, usePlugin, usePluginTheme, useRemotePlugins, useSurfaceStore };
|
|
3213
|
+
export { ActionButton, type ActivitySnapshot, AdaOrb, type AdaOrbState, Badge, type BiometricsAPI, type BiometricsAPIv2, type BluetoothAPI, type BluetoothDevice, type Bridge, type BundleCacheEntry, type BundleStorage, type CameraAPI, CameraPreview, type CameraPreviewProps, type CapabilitySnapshot, type CapabilityStatus, Card, type Confidence, type ConnectivityAPI, type ConnectivitySnapshot, type CookiesAPI, type DeviceBridgeDomain, type DeviceContextSnapshot, type DeviceState, type DeviceToolContext, type DeviceToolDefinition, type DeviceToolFn, type DeviceToolOutcome, type DeviceToolSummary, type DeviceToolsAction, type DeviceToolsSessionDeps, EmptyState, type EnvironmentSnapshot, ErrorState, type FileSystemAPI, type Freshness, type HapticsAPI, type InlineSurfaceManifest, type InteractionSnapshot, ListItem, type LoadedBundleInfo, type LoadedBundleSource, LoadingState, type LocationAPI, type MapsAPI, type MapsAddress, type MapsCoordinate, type MapsPlace, type MapsRoute, type MapsRouteStep, MemoryBundleStorage, type MountSurfaceManifest, type NativeCapabilities$1 as NativeCapabilities, type NfcAPI, type NotificationsAPI, PLATFORM_VOICE_CALLER, type PluginManifest as PlatformPluginManifest, type PluginBridge, type PluginContext, type PluginGrants, type PluginHooks$1 as PluginHooks, type PluginManifest$1 as PluginManifest, type PluginNative, type PluginProps, type PluginRuntimeProps, type PluginRuntimeSource, PluginSDK, type PluginSummary, type PluginSurfaceMeta, type RegisteredPlugin, type RegisteredServicePlugin, type RegisteredToolsPlugin, type RegisteredUIPlugin, type RemotePlugin, type RenderMode, type ScannerAPI, SearchBar, type SessionSnapshot, type SharingAPI, type SpatialEvent, type SpatialIntent, type SpatialPoint, type SpatialSnapshot, type SpatialStateEvent, type StateEvent, type StateListener, type StateQuality, type StorageAPI, type Subscription, SurfaceCard, type SurfaceCardProps, SurfaceDispatcher, SurfaceDrawer, type SurfaceDrawerProps, SurfaceFullscreen, type SurfaceFullscreenProps, type SurfaceManifest, type SurfacePlacement, type SurfaceType, type SurfaceVisibility, TabBar, type ThemeTokens, type TrustedToolDeclaration, type VisionAPI, type VisionFrameQuality, type VisionLabel, type VisionObject, type VisionText, type VisualSnapshot, type VoiceAPI, type VoiceCaller, type VoiceEvent, type VoicePersona, type VoiceSessionProvider, type VoiceSnapshot, type VoiceStartOptions, type VoiceState, type VoiceSubscription, type VoiceTranscriptEntry, type WebViewCookie, __resetSessionSyncForTest, bundleManager, callDeviceTool, clearDeviceTools, createNativeBridge, deviceState as deviceStateV2, deviceToolsActionForSession, ensureDeviceToolsLoaded, explainCapability, gateDeviceState, gatePluginNative, gateVoice, getCameraPreviewProvider, getHaptics, getNativeBundleHash, getNativeBundleUrl, getNativeComponentId, getPlugin, getPluginNative, handleServerDeviceToolCall, hasDeviceTool, implementedStateDomains, implementedVoice, isActionable, isDeviceToolCommand, isInlineSurface, isMountSurface, listDeviceTools, listPlugins, nativePluginRegistry, neverObserved, register, registerBundleModules, registerDeviceTools, registerNativePlugin, registerPlatformDeviceTools, registerService, registerTools, resetDeviceToolLoader, resolvePluginGrants, resolvePluginRuntimeProps, resolvePluginUrl, setBundleStorage, setCameraPreviewProvider, setDeviceStateProvider, setDeviceToolContextProvider, setDeviceToolResultPoster, setHapticsProvider, setPluginNativeProvider, setPluginThemeColors, setVoiceProvider, supportsNative, supportsWebView, syncDeviceToolsForSession, unregisterDeviceTools, unregisterNativePlugin, useApi, useNativeBridge, usePlugin, usePluginTheme, useRemotePlugins, useSurfaceStore };
|