@mhome/app-facade-protocol 1.64.0 → 1.69.0

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/README.md CHANGED
@@ -17,6 +17,9 @@ owns public targets, request/response/event schemas, and stable operation
17
17
  identity. Core-to-Node routes and provider runtime models belong to
18
18
  `mhome-core-api` instead.
19
19
 
20
+ See [resource definition, observation, history and relationship reads](contract/resource-read-v1.md)
21
+ for shared business reads and the separate UI rendering boundary.
22
+
20
23
  ## Business DTO ownership
21
24
 
22
25
  A Facade owns the public business DTO for an operation. Native Agent tools, MCP,
@@ -41,7 +44,7 @@ Composition does not imply a distributed transaction or automatic rollback.
41
44
  ## App result contract
42
45
 
43
46
  `control.responseDetail` selects information depth: `full` (default) returns
44
- `data`, while `essential` returns YAML in `text`. It is independent of action
47
+ `data`, while `essential` returns YAML in `data`. It is independent of action
45
48
  `mode` and is never copied into business input, prepared snapshots or execution
46
49
  fingerprints. `responseFormat` and business `outputFormat` are rejected.
47
50
 
@@ -49,14 +52,18 @@ Final responses use exactly one of these shapes:
49
52
 
50
53
  ```json
51
54
  {"status":"success","data":{"items":[]}}
52
- {"status":"success","text":"items: []\n"}
55
+ {"status":"success","data":"items: []\n"}
53
56
  {"status":"partial","data":{"moved":["a"],"notMoved":[{"deviceId":"b","reason":"offline"}]}}
54
57
  {"status":"error","error":{"code":"PRECONDITION_FAIL","message":"The object changed"}}
55
58
  {"status":"cancelled"}
56
59
  ```
57
60
 
58
- Success and partial carry exactly one of `data` or `text`. Error carries a
59
- `code`, `message`, and optional `details`; success, partial and cancellation may
61
+ Success may omit `data` for an acknowledgement; partial always carries `data`.
62
+ The request selects the representation: Full is the business value (which may itself
63
+ be a string); Essential is a YAML string. Do not infer response detail from its type.
64
+ Pure acknowledgements are identical in both modes: `{"status":"success"}`. Error carries a
65
+ `code`, `message`, and optional Full `details`. Essential errors keep code/message
66
+ and place their projected diagnostics in YAML `data`; success, partial and cancellation may
60
67
  carry structured `warnings`. Resource state belongs inside data: successfully
61
68
  reading a failed job is still a successful invocation. Partial is not complete
62
69
  success and must remain visible to the caller. Transport admission failures keep
@@ -69,8 +76,15 @@ execution or replay. Reject returns cancellation. Response detail never changes
69
76
  approval meaning or permission checks. Operation and HITL continuation protocols
70
77
  are separate from this result envelope.
71
78
 
72
- App UI requests full and unwraps the envelope at its transport boundary. Agent
73
- and MCP public results use essential; internal workflow reads use full. The CLI
79
+ App UI requests full and preserves the complete envelope through transport and SDK
80
+ parsing. UI consumers explicitly read data and handle warnings and non-success outcomes.
81
+ Essential YAML omits null-valued object fields recursively after domain projection.
82
+ This shared rule also applies to error diagnostics and visible warning details.
83
+ Full values, false, zero, strings (including empty strings), empty collections,
84
+ array positions and a root null value are preserved. This is output-only; it
85
+ never changes request semantics, stored data or prepared actions.
86
+
87
+ Agent and MCP public results use essential; internal workflow reads use full. The CLI
74
88
  uses `--response-detail full|essential`, and the bundled external Skill requests
75
89
  essential. Dashboard essential projection keeps card identity, title, type,
76
90
  view choices, excluding renderer configuration and resource payloads. Card list
@@ -218,3 +232,20 @@ Business operation permissions are declared once in [the authorization manifest]
218
232
  `/app/matter/device/list` is the management inventory, grouped by Fabric node. It is distinct from the Plugin's Integration enumeration `/matter/device/list`. Its `sourceDeviceId` values identify Plugin projections, not Twin entities. Nodes without projected devices remain visible; `membershipComplete`, `inventoryComplete`, per-node completeness and `errors` describe partial observations. Lists omit endpoints and attribute payloads.
219
233
 
220
234
  `/app/matter/fabric/node/detail` accepts 1–20 unique `matterNodeIds`, returning an ordered item result or error for each ID. The request uses string IDs without numeric coercion. See `schema/matter-management.v1.schema.json`. All `/app/matter/` operations execute through the Space's Hub irrespective of scope execution mode. Node removal is a manual UI/CLI capability, not a delegated Agent/MCP tool.
235
+
236
+ UI Integration metadata uses `/app/integration/presentation/get`; see
237
+ [the presentation contract](contract/integration-presentation-v1.md).
238
+
239
+ ## Artifact delivery
240
+
241
+ `/app/artifact/resolve` follows the Space execution route and can return a Host-local
242
+ URL with its TLS identity. `/app/artifact/external/resolve` is an explicit cloud-only
243
+ read using the same `ResolveArtifactRequest` and `ResolveArtifactResponse` contract.
244
+ It returns a short-lived signed HTTPS URL through an externally reachable cloud
245
+ origin. Its deadline is capped by the artifact's expiry at issuance. Issuing a URL
246
+ does not renew retention; reading local temporary content retains the owner's
247
+ normal idle-renewal behavior without extending an already issued URL's deadline.
248
+ Cloud-owned content uses an object-store signed URL; local content is read from its
249
+ owning Hub on demand. External resolution creates no cloud replica and cannot
250
+ make an offline local owner readable. The current inline Hub bridge size limit
251
+ still applies. Local file download remains a client workflow over ordinary resolve.
@@ -18,5 +18,5 @@ are performed. Current Twin states still have their separate read/subscription.
18
18
 
19
19
  UI uses `control: {mode: "direct", responseDetail: "full"}` and receives the shared
20
20
  `{status: "success", data: ...}` envelope. The generic essential projection returns
21
- `{status: "success", text: ...}` with the same business fields rendered as YAML;
21
+ `{status: "success", data: ...}` with the same business fields rendered as YAML;
22
22
  there is no domain field reduction. This is not the compact business detail API.
@@ -0,0 +1,27 @@
1
+ # Integration UI presentation
2
+
3
+ `/app/integration/presentation/get` is a read-only App Facade for UI consumers.
4
+ Input is `{integrationId}` for one root Integration. Tenant, Space and caller
5
+ come from authenticated context; Space read membership is required. Empty IDs,
6
+ unknown fields and Provider IDs are rejected; missing Integrations are not found.
7
+ This operation is not registered in Agent, MCP or CLI catalogs.
8
+
9
+ The Space execution authority performs the complete read. Integration names and
10
+ icons include the root and every Provider in its visible catalog, regardless of
11
+ installation. `typeNames` labels entity types; `entityStandalone` indicates
12
+ independent entity presentation; `groupSupported` indicates type-level group
13
+ support. Neither boolean map describes an instance's active state. `twinNames`
14
+ labels the root's applicable production Twin templates, not entity instances.
15
+ Developer draft overrides belong to developer tooling, not ordinary App display.
16
+ `capabilities` uses the shared CapabilityDescriptor, including full IDs and
17
+ visibility. No scripts, entity instances, live states or connection data are read.
18
+
19
+ Collections are always present and empty when the model has no corresponding
20
+ metadata. Optional empty names/icons are omitted. Upstream failures propagate;
21
+ existing metadata cache refresh policies remain in effect. No installation,
22
+ entity synchronization or business writes are performed.
23
+
24
+ UI sends `control: {mode: "direct", responseDetail: "full"}` and receives
25
+ `{status: "success", data: ...}`. Essential retains the same business fields in
26
+ YAML `data`; this interface has no separate field reduction. Mutating control
27
+ modes are not supported. SDK indexes the capability array by full ID for UI use.
@@ -0,0 +1,75 @@
1
+ # Resource read boundaries
2
+
3
+ Business reads separate persisted definitions/configuration from current observations,
4
+ chronological history, and one-hop resource relationships. Tenant and Space are taken
5
+ from authenticated request context. All targets require Space read authority and
6
+ follow the Space execution authority on both local Core and cloud.
7
+
8
+ ## Shared business reads
9
+
10
+ | Target | Input | Business data |
11
+ | --- | --- | --- |
12
+ | `/app/device/detail/get` | `{ids}` | Existing ordered `items` with `device` definitions, capabilities, group membership, enabled/source metadata; no online or state fields |
13
+ | `/app/device/state/get` | `{ids}` | Ordered `items` with `resource: {entityId, online?, onlineUpdatedAtSec?, latest}` |
14
+ | `/app/widget/detail/get` | `{ids}` | Existing ordered `items` with `widget` configuration; no value, next or nextTimestamp |
15
+ | `/app/widget/value/get` | `{ids}` | Ordered `items` with `resource: {widgetId, revision, value, next?, nextTimestamp?}` |
16
+ | `/app/automation/status` | `{ids}` | Ordered `items` with `resource: {automationId, enabled, runnable, running, blocked, lastRunAt}` |
17
+ | `/app/graph/relations/get` | `{resource: {kind, id}}` | `{resource, relations: [{from, relation, to, capabilityIds?}]}` |
18
+
19
+ Observation batches accept 1–100 IDs, reject blanks, trim and deduplicate preserving
20
+ request order. Every normalized ID gets `{requestedId, found, resource?}`; missing
21
+ resources have `found: false, reason: "not_found"`. A known Device with no observations
22
+ has `found: true` and `latest: []`. Unknown reachability omits `online`; it is not false.
23
+ Device state supports visible physical devices and Device groups. Automation status
24
+ accepts user program IDs, including retained one-time programs, not groups or internal
25
+ Twin programs. Widget value supports Data, Button and Circadian resources. JSON null
26
+ is a value, not a missing resource. Button values may be null.
27
+
28
+ `latest` contains one record per capability: `capabilityId`, optional `state`, `event`,
29
+ `log`. These preserve the decoded Twin observation objects and their second timestamps.
30
+ They contain no persistence tenant/Space keys. Reads use stored latest snapshots and
31
+ retain the existing history fallback on cache miss; they do not refresh providers.
32
+ Definition reads do not load observations, history, related programs or visible Cards.
33
+
34
+ Graph kinds are `device`, `entity`, `automation`, `widget`, `card`. Directional relations
35
+ are Automation `uses` Device/Entity (with capability IDs), Automation `owns` Widget,
36
+ visible Card `presents` a resource, and generated Automation `implements` Device.
37
+ Generated program references are not editable user programs. Existing Managers and
38
+ indexes own these relationships; this read introduces no graph database or traversal.
39
+ It returns IDs, not recursively expanded definitions, scripts, names or runtime data.
40
+ Hidden Device render anchors are not public Card references.
41
+
42
+ Full uses the standard `{status, data}` result with structured business data. Essential
43
+ uses the same envelope with YAML `data`; timestamps are presented in the Home timezone.
44
+ Device list additionally omits connection/manufacturer metadata in Essential. New
45
+ observation and graph reads preserve their bounded business fields in Essential.
46
+
47
+ ## UI-only reads
48
+
49
+ - `/app/device/view/get`, `{deviceId}`: `{deviceId, primaryViewId, views, widgets}`.
50
+ Reads the unique hidden Device render definition, preserving exact Widget/Twin
51
+ bindings. Disabled devices can have empty definitions. Multiple anchors fail rather
52
+ than choosing an arbitrary visible Card. Widgets contain configuration, never values.
53
+ - `/app/device/history/series/get`, `{deviceId, capabilityIds?: []}`:
54
+ `{deviceId, series}` with per-capability decoded `states`, `events`, `logs`. Empty
55
+ capability selection reads all series. This preserves chart structure; the shared
56
+ Device history Facade remains a bounded, merged business timeline.
57
+ - `/app/dashboard/canvas/content/get`, `{canvasId}`: Canvas metadata plus Cards' static
58
+ titles, view bindings, layouts, settings, metadata and selected Widget definitions.
59
+ No observations, history, related automations or suggestions are loaded.
60
+
61
+ The frontend batches selected observations after reading definitions. Automation Cards
62
+ also read selected Automation definitions for their name and type. Canvas IDs are
63
+ split into bounded batches and deduplicated across Cards. History is requested by an
64
+ open detail renderer; relationships by the view that displays them. Pushes received
65
+ during a snapshot request are applied after it lands. Switching scopes or disposing
66
+ controllers prevents late responses from landing in another Space.
67
+
68
+ House continues to use `/app/entity/presentation/get` and legacy Twin reads as its
69
+ separate development boundary. Pod Deck retains its existing complete Card content
70
+ reader. These consumers are not silently migrated to static Canvas responses.
71
+
72
+ Generated Twin program inspection retains its internal `/automation/get`,
73
+ `/automation/status` and `/automation/info` UI readers. These IDs are not accepted
74
+ by the shared user-program Automation detail/status contract. User program detail
75
+ pages use the shared definition, status and graph readers.
@@ -0,0 +1,383 @@
1
+ {
2
+ "valid": [
3
+ {
4
+ "definition": "ListGroupsInput",
5
+ "value": {}
6
+ },
7
+ {
8
+ "definition": "GetGroupsInput",
9
+ "value": {
10
+ "ids": [
11
+ "g:1",
12
+ "g:1",
13
+ "missing"
14
+ ]
15
+ }
16
+ },
17
+ {
18
+ "definition": "GroupPresentationInput",
19
+ "value": {
20
+ "groupId": "g:1"
21
+ }
22
+ },
23
+ {
24
+ "definition": "CreateGroupInput",
25
+ "value": {
26
+ "creationId": "11111111-1111-4111-8111-111111111111",
27
+ "name": "Lights",
28
+ "memberType": "basic.device.Device",
29
+ "memberIds": [
30
+ "lamp-a",
31
+ "lamp-b",
32
+ "lamp-a"
33
+ ]
34
+ }
35
+ },
36
+ {
37
+ "definition": "RenameGroupInput",
38
+ "value": {
39
+ "groupId": "g:1",
40
+ "name": "Renamed",
41
+ "expectedRevision": 3
42
+ }
43
+ },
44
+ {
45
+ "definition": "UpdateGroupMembersInput",
46
+ "value": {
47
+ "groupId": "g:1",
48
+ "memberIds": [
49
+ "lamp-b"
50
+ ],
51
+ "expectedRevision": 3
52
+ }
53
+ },
54
+ {
55
+ "definition": "DeleteGroupInput",
56
+ "value": {
57
+ "groupId": "g:1",
58
+ "expectedRevision": 3
59
+ }
60
+ },
61
+ {
62
+ "definition": "GroupSummary",
63
+ "value": {
64
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
65
+ "name": "Lights",
66
+ "memberType": "basic.device.Device",
67
+ "memberCount": 2,
68
+ "revision": 3,
69
+ "applyState": "ready",
70
+ "createdAt": 1900000000000,
71
+ "updatedAt": 1900000001000
72
+ }
73
+ },
74
+ {
75
+ "definition": "GroupDetail",
76
+ "value": {
77
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
78
+ "name": "Lights",
79
+ "memberType": "basic.device.Device",
80
+ "memberCount": 2,
81
+ "revision": 3,
82
+ "applyState": "ready",
83
+ "createdAt": 1900000000000,
84
+ "updatedAt": 1900000001000,
85
+ "memberIds": [
86
+ "lamp-a",
87
+ "lamp-b"
88
+ ],
89
+ "capabilities": [
90
+ {
91
+ "id": "basic.device.command.onOff",
92
+ "memberCount": 1
93
+ }
94
+ ],
95
+ "memberProblems": [
96
+ {
97
+ "memberId": "lamp-b",
98
+ "code": "MEMBER_UNAVAILABLE",
99
+ "message": "Member is unavailable"
100
+ }
101
+ ]
102
+ }
103
+ },
104
+ {
105
+ "definition": "GroupMutationOutput",
106
+ "value": {
107
+ "groupId": "g:1",
108
+ "revision": 4,
109
+ "applyState": "failed",
110
+ "applicationProblem": {
111
+ "code": "GROUP_APPLY_FAILED",
112
+ "message": "Retry application"
113
+ }
114
+ }
115
+ },
116
+ {
117
+ "definition": "DeleteGroupOutput",
118
+ "value": {
119
+ "groupId": "g:1",
120
+ "deleted": true
121
+ }
122
+ },
123
+ {
124
+ "definition": "ListGroupsOutput",
125
+ "value": {
126
+ "spaceId": "space",
127
+ "groups": [
128
+ {
129
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
130
+ "name": "Lights",
131
+ "memberType": "basic.device.Device",
132
+ "memberCount": 2,
133
+ "revision": 3,
134
+ "applyState": "ready",
135
+ "createdAt": 1900000000000,
136
+ "updatedAt": 1900000001000
137
+ }
138
+ ]
139
+ }
140
+ },
141
+ {
142
+ "definition": "GetGroupsOutput",
143
+ "value": {
144
+ "spaceId": "space",
145
+ "items": [
146
+ {
147
+ "requestedId": "g:11111111-1111-4111-8111-111111111111",
148
+ "found": true,
149
+ "group": {
150
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
151
+ "name": "Lights",
152
+ "memberType": "basic.device.Device",
153
+ "memberCount": 2,
154
+ "revision": 3,
155
+ "applyState": "ready",
156
+ "createdAt": 1900000000000,
157
+ "updatedAt": 1900000001000,
158
+ "memberIds": [
159
+ "lamp-a",
160
+ "lamp-b"
161
+ ],
162
+ "capabilities": [
163
+ {
164
+ "id": "basic.device.command.onOff",
165
+ "memberCount": 1
166
+ }
167
+ ],
168
+ "memberProblems": [
169
+ {
170
+ "memberId": "lamp-b",
171
+ "code": "MEMBER_UNAVAILABLE",
172
+ "message": "Member is unavailable"
173
+ }
174
+ ]
175
+ }
176
+ },
177
+ {
178
+ "requestedId": "missing",
179
+ "found": false,
180
+ "reason": "not_found"
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ ],
186
+ "invalid": [
187
+ {
188
+ "definition": "RenameGroupInput",
189
+ "value": {
190
+ "groupId": "g:1",
191
+ "name": "Renamed"
192
+ }
193
+ },
194
+ {
195
+ "definition": "UpdateGroupMembersInput",
196
+ "value": {
197
+ "groupId": "g:1",
198
+ "memberIds": [
199
+ "lamp-b"
200
+ ],
201
+ "expectedRevision": 3,
202
+ "name": "Unexpected"
203
+ }
204
+ },
205
+ {
206
+ "definition": "DeleteGroupInput",
207
+ "value": {
208
+ "groupId": "g:1",
209
+ "expectedRevision": 0
210
+ }
211
+ },
212
+ {
213
+ "definition": "CreateGroupInput",
214
+ "value": {
215
+ "creationId": "11111111-1111-4111-8111-111111111111",
216
+ "name": "Lights",
217
+ "memberType": "basic.device.Device",
218
+ "memberIds": [
219
+ "lamp-a",
220
+ "lamp-b",
221
+ "lamp-a"
222
+ ],
223
+ "members": [
224
+ {
225
+ "data": "internal"
226
+ }
227
+ ]
228
+ }
229
+ },
230
+ {
231
+ "definition": "GroupDetail",
232
+ "value": {
233
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
234
+ "name": "Lights",
235
+ "memberType": "basic.device.Device",
236
+ "memberCount": 2,
237
+ "revision": 3,
238
+ "applyState": "ready",
239
+ "createdAt": 1900000000000,
240
+ "updatedAt": 1900000001000,
241
+ "memberIds": [
242
+ "lamp-a",
243
+ "lamp-b"
244
+ ],
245
+ "capabilities": [
246
+ {
247
+ "id": "basic.device.command.onOff",
248
+ "memberCount": 1
249
+ }
250
+ ],
251
+ "memberProblems": [
252
+ {
253
+ "memberId": "lamp-b",
254
+ "code": "MEMBER_UNAVAILABLE",
255
+ "message": "Member is unavailable"
256
+ }
257
+ ],
258
+ "twinSig": "internal"
259
+ }
260
+ },
261
+ {
262
+ "definition": "GetGroupsInput",
263
+ "value": {
264
+ "ids": [
265
+ "a",
266
+ "a",
267
+ "a",
268
+ "a",
269
+ "a",
270
+ "a",
271
+ "a",
272
+ "a",
273
+ "a",
274
+ "a",
275
+ "a",
276
+ "a",
277
+ "a",
278
+ "a",
279
+ "a",
280
+ "a",
281
+ "a",
282
+ "a",
283
+ "a",
284
+ "a",
285
+ "a",
286
+ "a",
287
+ "a",
288
+ "a",
289
+ "a",
290
+ "a",
291
+ "a",
292
+ "a",
293
+ "a",
294
+ "a",
295
+ "a",
296
+ "a",
297
+ "a",
298
+ "a",
299
+ "a",
300
+ "a",
301
+ "a",
302
+ "a",
303
+ "a",
304
+ "a",
305
+ "a",
306
+ "a",
307
+ "a",
308
+ "a",
309
+ "a",
310
+ "a",
311
+ "a",
312
+ "a",
313
+ "a",
314
+ "a",
315
+ "a",
316
+ "a",
317
+ "a",
318
+ "a",
319
+ "a",
320
+ "a",
321
+ "a",
322
+ "a",
323
+ "a",
324
+ "a",
325
+ "a",
326
+ "a",
327
+ "a",
328
+ "a",
329
+ "a",
330
+ "a",
331
+ "a",
332
+ "a",
333
+ "a",
334
+ "a",
335
+ "a",
336
+ "a",
337
+ "a",
338
+ "a",
339
+ "a",
340
+ "a",
341
+ "a",
342
+ "a",
343
+ "a",
344
+ "a",
345
+ "a",
346
+ "a",
347
+ "a",
348
+ "a",
349
+ "a",
350
+ "a",
351
+ "a",
352
+ "a",
353
+ "a",
354
+ "a",
355
+ "a",
356
+ "a",
357
+ "a",
358
+ "a",
359
+ "a",
360
+ "a",
361
+ "a",
362
+ "a",
363
+ "a",
364
+ "a",
365
+ "a"
366
+ ]
367
+ }
368
+ },
369
+ {
370
+ "definition": "GroupSummary",
371
+ "value": {
372
+ "groupId": "g:11111111-1111-4111-8111-111111111111",
373
+ "name": "Lights",
374
+ "memberType": "basic.device.Device",
375
+ "memberCount": 2,
376
+ "revision": 3,
377
+ "applyState": "unknown",
378
+ "createdAt": 1900000000000,
379
+ "updatedAt": 1900000001000
380
+ }
381
+ }
382
+ ]
383
+ }