@exponential_app/engine 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/NOTICE +54 -0
- package/README.md +35 -2
- package/dist/capabilities.js +29 -0
- package/dist/capabilities.js.map +9 -0
- package/dist/chunks/index-3nf9f7pv.js +315 -0
- package/dist/chunks/index-3nf9f7pv.js.map +10 -0
- package/dist/chunks/index-3rgr16n2.js +443 -0
- package/dist/chunks/index-3rgr16n2.js.map +9 -0
- package/dist/chunks/index-79swfbsm.js +366 -0
- package/dist/chunks/index-79swfbsm.js.map +10 -0
- package/dist/chunks/index-knntt4cb.js +259 -0
- package/dist/chunks/index-knntt4cb.js.map +10 -0
- package/dist/chunks/index-m2845dn6.js +72 -0
- package/dist/chunks/index-m2845dn6.js.map +10 -0
- package/dist/chunks/index-pqpnz4ns.js +2213 -0
- package/dist/chunks/index-pqpnz4ns.js.map +9 -0
- package/dist/chunks/index-q4zws02e.js +2301 -0
- package/dist/chunks/index-q4zws02e.js.map +10 -0
- package/dist/chunks/index-s3xjr701.js +170 -0
- package/dist/chunks/index-s3xjr701.js.map +10 -0
- package/dist/chunks/index-sk8b0dh4.js +126 -0
- package/dist/chunks/index-sk8b0dh4.js.map +10 -0
- package/dist/chunks/index-tvsykgcv.js +3015 -0
- package/dist/chunks/index-tvsykgcv.js.map +19 -0
- package/dist/chunks/index-v0m7r4y3.js +6869 -0
- package/dist/chunks/index-v0m7r4y3.js.map +13 -0
- package/dist/consent.js +24 -0
- package/dist/consent.js.map +9 -0
- package/dist/index.js +654 -0
- package/dist/index.js.map +12 -0
- package/dist/lifecycle.js +11 -0
- package/dist/lifecycle.js.map +9 -0
- package/dist/outbox.js +27 -0
- package/dist/outbox.js.map +9 -0
- package/dist/pdf.js +15 -0
- package/dist/pdf.js.map +9 -0
- package/dist/providers.js +26 -0
- package/dist/providers.js.map +9 -0
- package/dist/runner.js +24 -0
- package/dist/runner.js.map +9 -0
- package/dist/tools.js +38 -0
- package/dist/tools.js.map +9 -0
- package/dist/types/domain-contract/src/app-records.d.ts +3148 -0
- package/dist/types/domain-contract/src/app-share.d.ts +222 -0
- package/dist/types/domain-contract/src/apps.d.ts +612 -0
- package/dist/types/eengine/fixtures/providers.json +55 -0
- package/dist/types/eengine/src/broker.d.ts +80 -0
- package/dist/types/eengine/src/capabilities.d.ts +88 -0
- package/dist/types/eengine/src/consent.d.ts +50 -0
- package/dist/types/eengine/src/index.d.ts +12 -0
- package/dist/types/eengine/src/instance.d.ts +131 -0
- package/dist/types/eengine/src/lifecycle.d.ts +64 -0
- package/dist/types/eengine/src/outbox.d.ts +3 -0
- package/dist/types/eengine/src/pdf.d.ts +36 -0
- package/dist/types/eengine/src/providers.d.ts +38 -0
- package/dist/types/eengine/src/runner.d.ts +82 -0
- package/dist/types/eengine/src/stores.d.ts +125 -0
- package/dist/types/eengine/src/synced.d.ts +59 -0
- package/dist/types/eengine/src/tools.d.ts +79 -0
- package/dist/types/eengine/src/web/index.d.ts +20 -0
- package/dist/types/eengine/src/web/mcp.d.ts +27 -0
- package/dist/types/eengine/src/web/platform.d.ts +91 -0
- package/dist/types/eengine/src/web/stores.d.ts +125 -0
- package/dist/types/emcp/fixtures/automations.json +521 -0
- package/dist/types/emcp/fixtures/hub.json +498 -0
- package/dist/types/emcp/src/access.d.ts +55 -0
- package/dist/types/emcp/src/audit.d.ts +22 -0
- package/dist/types/emcp/src/automations.d.ts +111 -0
- package/dist/types/emcp/src/exposes.d.ts +242 -0
- package/dist/types/emcp/src/hub.d.ts +63 -0
- package/dist/types/emcp/src/index.d.ts +7 -0
- package/dist/types/emcp/src/pure.d.ts +11 -0
- package/dist/types/emcp/src/upstream.d.ts +288 -0
- package/dist/types/emcp/src/vault.d.ts +34 -0
- package/dist/web/index.js +791 -0
- package/dist/web/index.js.map +13 -0
- package/fixtures/bundle.json +1165 -0
- package/fixtures/capabilities.json +434 -0
- package/fixtures/consent.json +146 -0
- package/fixtures/lifecycle.json +125 -0
- package/fixtures/outbox.json +22 -0
- package/fixtures/providers.json +55 -0
- package/fixtures/runner.json +2167 -0
- package/package.json +92 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.2): CONSENT. The package's capability manifest (format 5 `capabilities: [{id, why, origins?}]`) is the CEILING; the person's decision comes next (capabilities.json `brokerDecide`); the audit row last. GRANTS are per app: `{<scope id>: granted | denied}` (absent = undecided), kept ONLY for `ask` scopes (a `picker` scope's consent is the platform picker, every call). THE SHEET (`sheetCases`, `consentSheet`): when the broker answers `ask`, the Engine shows ONE genUI surface built from existing components only (Drawer + Text + ChoiceTiles + Button; no new primitive), the same on every platform and the default `ConsentUi` (a host may override the painter, never the rule): a bottom Drawer titled `Allow {app} to {verb}?` (`copy.title`; the scope's `verb`, an `origins` scope's `{origins}` = its origins joined with `, `), the manifest's `why` as a Text, ChoiceTiles `answer` (one column) with `copy.options` in order (`allow`, `once`, `deny`) starting on `allow`, and a Button `copy.confirm` (the default variant) whose press sends the event `consent.answer` with the picked value; closing the Drawer (Escape, the backdrop, a swipe) answers `dismiss`. THE ANSWER (`answerCases`, `consentAnswer`): `allow` = the call goes ahead and the grant is stored `granted`; `once` = the call goes ahead and nothing is stored (the next call asks again); `deny` = the call is refused `consentDenied` and `denied` is stored; `dismiss` = refused `consentDismissed`, nothing stored. MANAGE (`manageCases`, `manageSheet`): the app's Permissions surface (its … menu, the runner's app menu) = a Drawer `copy.manageTitle` with one Switch per DECLARED `ask` scope in manifest order (label = the capability's `label`, description = the `why`, checked = granted; event `consent.toggle` with the scope) and no Switch for `picker` scopes; flipping one is the gesture: on = `granted`, off = REVOKE = the grant removed (undecided, so the next call asks). WRITES (`writeCases`, `grantWrite`): a grant changes ONLY through the ConsentUi after a user gesture (`via: consentUi`, `gesture: true`); anything else (a hub tool, an agent, the package, the host config, a sync) is refused `onlyConsentUi`, a ConsentUi write without a gesture `noGesture`, an undeclared or `picker` or `hostOnly` scope `notAsk`. PERSISTENCE (`storeCases`, `grantsRecord` / `grantsOnPackage`): the Vault (emcp `Vault`: Keychain, Keystore, libsecret, the server's sealed column) holds key `vaultKey` with `{app}` = the package id, value = canonical JSON `{grants, origins, signer, v}`: `v` = `recordVersion`, `signer` = the package signature's public key, `origins` = the `http.fetch` origins the grant was given for (sorted). On every package load the Engine reconciles (`grantsOnPackage`): another signer = a different author, every grant dropped; a scope the new version no longer declares (or now declares as non-ask) is dropped; an `http.fetch` grant whose declared origins are no longer a subset of the granted ones is dropped (new origins ask again); an unreadable record or another `v` = no grants (ask again, never fail open).",
|
|
3
|
+
"recordVersion": 1,
|
|
4
|
+
"vaultKey": "eengine/grants/{app}",
|
|
5
|
+
"copy": {
|
|
6
|
+
"title": "Allow {app} to {verb}?",
|
|
7
|
+
"options": [
|
|
8
|
+
{ "label": "Allow", "value": "allow" },
|
|
9
|
+
{ "label": "Allow once", "value": "once" },
|
|
10
|
+
{ "label": "Don't allow", "value": "deny" }
|
|
11
|
+
],
|
|
12
|
+
"confirm": "Continue",
|
|
13
|
+
"manageTitle": "Permissions"
|
|
14
|
+
},
|
|
15
|
+
"answerCases": [
|
|
16
|
+
{ "scope": "photos.read", "grants": {}, "answer": "allow", "expected": { "verdict": "call", "reason": null, "grants": { "photos.read": "granted" } } },
|
|
17
|
+
{ "scope": "photos.read", "grants": {}, "answer": "once", "expected": { "verdict": "call", "reason": null, "grants": {} } },
|
|
18
|
+
{ "scope": "photos.read", "grants": { "camera.capture": "granted" }, "answer": "deny", "expected": { "verdict": "denied", "reason": "consentDenied", "grants": { "camera.capture": "granted", "photos.read": "denied" } } },
|
|
19
|
+
{ "scope": "photos.read", "grants": {}, "answer": "dismiss", "expected": { "verdict": "denied", "reason": "consentDismissed", "grants": {} } }
|
|
20
|
+
],
|
|
21
|
+
"writeCases": [
|
|
22
|
+
{ "name": "a switch flipped on grants", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "consentUi", "gesture": true, "scope": "photos.read", "value": "granted" }, "expected": { "grants": { "photos.read": "granted" }, "refused": null } },
|
|
23
|
+
{ "name": "a switch flipped off revokes", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": { "photos.read": "granted" }, "write": { "via": "consentUi", "gesture": true, "scope": "photos.read", "value": null }, "expected": { "grants": {}, "refused": null } },
|
|
24
|
+
{ "name": "no gesture, no change", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "consentUi", "gesture": false, "scope": "photos.read", "value": "granted" }, "expected": { "grants": {}, "refused": "noGesture" } },
|
|
25
|
+
{ "name": "an agent cannot grant itself", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "agent", "gesture": true, "scope": "photos.read", "value": "granted" }, "expected": { "grants": {}, "refused": "onlyConsentUi" } },
|
|
26
|
+
{ "name": "a hub tool cannot grant", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "hubTool", "gesture": false, "scope": "photos.read", "value": "granted" }, "expected": { "grants": {}, "refused": "onlyConsentUi" } },
|
|
27
|
+
{ "name": "the package cannot grant", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "package", "gesture": false, "scope": "photos.read", "value": "granted" }, "expected": { "grants": {}, "refused": "onlyConsentUi" } },
|
|
28
|
+
{ "name": "the host config cannot grant", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "hostConfig", "gesture": false, "scope": "photos.read", "value": "granted" }, "expected": { "grants": {}, "refused": "onlyConsentUi" } },
|
|
29
|
+
{ "name": "an undeclared scope cannot be granted", "manifest": [{ "id": "photos.read", "why": "Pick the photos." }], "grants": {}, "write": { "via": "consentUi", "gesture": true, "scope": "camera.capture", "value": "granted" }, "expected": { "grants": {}, "refused": "notAsk" } },
|
|
30
|
+
{ "name": "a picker scope keeps no grant", "manifest": [{ "id": "share.send", "why": "Share an album." }], "grants": {}, "write": { "via": "consentUi", "gesture": true, "scope": "share.send", "value": "granted" }, "expected": { "grants": {}, "refused": "notAsk" } },
|
|
31
|
+
{ "name": "a host-only scope is never granted", "manifest": [{ "id": "shell.edit", "why": "Rearrange the panel." }], "grants": {}, "write": { "via": "consentUi", "gesture": true, "scope": "shell.edit", "value": "granted" }, "expected": { "grants": {}, "refused": "notAsk" } }
|
|
32
|
+
],
|
|
33
|
+
"storeCases": [
|
|
34
|
+
{
|
|
35
|
+
"name": "the record round-trips as canonical JSON",
|
|
36
|
+
"app": "at.exponential.photobook",
|
|
37
|
+
"signer": "pk1",
|
|
38
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }, { "id": "http.fetch", "why": "Rates.", "origins": ["https://b.example.com", "https://a.example.com"] }],
|
|
39
|
+
"grants": { "photos.read": "granted", "http.fetch": "granted" },
|
|
40
|
+
"expected": {
|
|
41
|
+
"key": "eengine/grants/at.exponential.photobook",
|
|
42
|
+
"value": "{\"grants\":{\"http.fetch\":\"granted\",\"photos.read\":\"granted\"},\"origins\":[\"https://a.example.com\",\"https://b.example.com\"],\"signer\":\"pk1\",\"v\":1}"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"reconcileCases": [
|
|
47
|
+
{
|
|
48
|
+
"name": "the same signer keeps what is still declared",
|
|
49
|
+
"stored": "{\"grants\":{\"camera.capture\":\"granted\",\"photos.read\":\"denied\"},\"origins\":[],\"signer\":\"pk1\",\"v\":1}",
|
|
50
|
+
"signer": "pk1",
|
|
51
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }],
|
|
52
|
+
"expected": { "photos.read": "denied" }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "another signer drops every grant",
|
|
56
|
+
"stored": "{\"grants\":{\"photos.read\":\"granted\"},\"origins\":[],\"signer\":\"pk1\",\"v\":1}",
|
|
57
|
+
"signer": "pk2",
|
|
58
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }],
|
|
59
|
+
"expected": {}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "new origins ask again",
|
|
63
|
+
"stored": "{\"grants\":{\"http.fetch\":\"granted\"},\"origins\":[\"https://a.example.com\"],\"signer\":\"pk1\",\"v\":1}",
|
|
64
|
+
"signer": "pk1",
|
|
65
|
+
"manifest": [{ "id": "http.fetch", "why": "Rates.", "origins": ["https://a.example.com", "https://b.example.com"] }],
|
|
66
|
+
"expected": {}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "fewer origins keep the grant",
|
|
70
|
+
"stored": "{\"grants\":{\"http.fetch\":\"granted\"},\"origins\":[\"https://a.example.com\",\"https://b.example.com\"],\"signer\":\"pk1\",\"v\":1}",
|
|
71
|
+
"signer": "pk1",
|
|
72
|
+
"manifest": [{ "id": "http.fetch", "why": "Rates.", "origins": ["https://b.example.com"] }],
|
|
73
|
+
"expected": { "http.fetch": "granted" }
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "a scope that became a picker scope is dropped",
|
|
77
|
+
"stored": "{\"grants\":{\"share.send\":\"granted\"},\"origins\":[],\"signer\":\"pk1\",\"v\":1}",
|
|
78
|
+
"signer": "pk1",
|
|
79
|
+
"manifest": [{ "id": "share.send", "why": "Share." }],
|
|
80
|
+
"expected": {}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "another record version asks again",
|
|
84
|
+
"stored": "{\"grants\":{\"photos.read\":\"granted\"},\"origins\":[],\"signer\":\"pk1\",\"v\":2}",
|
|
85
|
+
"signer": "pk1",
|
|
86
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }],
|
|
87
|
+
"expected": {}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "an unreadable record asks again",
|
|
91
|
+
"stored": "not json",
|
|
92
|
+
"signer": "pk1",
|
|
93
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }],
|
|
94
|
+
"expected": {}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "no record yet",
|
|
98
|
+
"stored": null,
|
|
99
|
+
"signer": "pk1",
|
|
100
|
+
"manifest": [{ "id": "photos.read", "why": "Pick the photos." }],
|
|
101
|
+
"expected": {}
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"sheetCases": [
|
|
105
|
+
{
|
|
106
|
+
"name": "the photos sheet",
|
|
107
|
+
"app": "Photobook",
|
|
108
|
+
"entry": { "id": "photos.read", "why": "Pick the photos for an album." },
|
|
109
|
+
"expected": [
|
|
110
|
+
{ "id": "root", "component": "Drawer", "title": "Allow Photobook to access your photos?", "open": true, "side": "bottom", "children": ["why", "answer"], "slots": { "footer": "confirm" }, "on": { "change": { "event": { "name": "consent.answer", "context": { "answer": "dismiss" } } } } },
|
|
111
|
+
{ "id": "why", "component": "Text", "text": "Pick the photos for an album." },
|
|
112
|
+
{ "id": "answer", "component": "ChoiceTiles", "name": "answer", "columns": 1, "value": { "@path": "/answer" }, "options": [{ "label": "Allow", "value": "allow" }, { "label": "Allow once", "value": "once" }, { "label": "Don't allow", "value": "deny" }] },
|
|
113
|
+
{ "id": "confirm", "component": "Button", "label": "Continue", "on": { "press": { "event": { "name": "consent.answer", "context": { "answer": { "@path": "/answer" } } } } } }
|
|
114
|
+
],
|
|
115
|
+
"data": { "answer": "allow" }
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "an origins scope names its origins",
|
|
119
|
+
"app": "Rates",
|
|
120
|
+
"entry": { "id": "http.fetch", "why": "Load exchange rates.", "origins": ["https://api.example.com", "https://backup.example.com"] },
|
|
121
|
+
"expected": [
|
|
122
|
+
{ "id": "root", "component": "Drawer", "title": "Allow Rates to connect to https://api.example.com, https://backup.example.com?", "open": true, "side": "bottom", "children": ["why", "answer"], "slots": { "footer": "confirm" }, "on": { "change": { "event": { "name": "consent.answer", "context": { "answer": "dismiss" } } } } },
|
|
123
|
+
{ "id": "why", "component": "Text", "text": "Load exchange rates." },
|
|
124
|
+
{ "id": "answer", "component": "ChoiceTiles", "name": "answer", "columns": 1, "value": { "@path": "/answer" }, "options": [{ "label": "Allow", "value": "allow" }, { "label": "Allow once", "value": "once" }, { "label": "Don't allow", "value": "deny" }] },
|
|
125
|
+
{ "id": "confirm", "component": "Button", "label": "Continue", "on": { "press": { "event": { "name": "consent.answer", "context": { "answer": { "@path": "/answer" } } } } } }
|
|
126
|
+
],
|
|
127
|
+
"data": { "answer": "allow" }
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"manageCases": [
|
|
131
|
+
{
|
|
132
|
+
"name": "one switch per declared ask scope",
|
|
133
|
+
"manifest": [
|
|
134
|
+
{ "id": "photos.read", "why": "Pick the photos for an album." },
|
|
135
|
+
{ "id": "share.send", "why": "Share a finished album." },
|
|
136
|
+
{ "id": "notifications.post", "why": "Remind you of due pages." }
|
|
137
|
+
],
|
|
138
|
+
"grants": { "photos.read": "granted", "notifications.post": "denied" },
|
|
139
|
+
"expected": [
|
|
140
|
+
{ "id": "root", "component": "Drawer", "title": "Permissions", "open": true, "side": "bottom", "children": ["scope_0", "scope_1"] },
|
|
141
|
+
{ "id": "scope_0", "component": "Switch", "label": "Photos", "description": "Pick the photos for an album.", "checked": true, "on": { "change": { "event": { "name": "consent.toggle", "context": { "scope": "photos.read" } } } } },
|
|
142
|
+
{ "id": "scope_1", "component": "Switch", "label": "Notifications", "description": "Remind you of due pages.", "checked": false, "on": { "change": { "event": { "name": "consent.toggle", "context": { "scope": "notifications.post" } } } } }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.1): AN APP INSTANCE'S LIFECYCLE, one pure reducer (`lifecycleStep(state, event, pkg)` -> `{state, effects}`) every host drives with its platform's events (the `WindowHost` reports them; the Engine never asks the OS). STATE = `{phase, screen, params}`: `phase` = `stopped` | `foreground` | `background`; `screen`/`params` = the open screen (null while stopped). EVENTS: `launch {screen?, params?}` (a plain start: the LAST screen the instance persisted, else the first in the navigation, via genUI's `resolveScreen`); `foreground`; `background`; `terminate`; `online` (the host's connection came back); `deepLink {screen, params}` (a URL from OUTSIDE the app: another app, the OS, a person's link in a standalone runner (`<bundle id>://<screen>?<params>`, parsed by the host); it opens only what genUI's `crossAppTarget` allows: an `exposed` screen with its declared params); `notificationTap {app, screen, params}` (a notification the app itself posted: `resolveScreen`, any declared screen, nav: false ones too; a tap carrying another app's id is refused `otherApp`). A deepLink or notificationTap while `stopped` is a COLD LAUNCH into its target. EFFECTS, in order: `{effect: open, screen, params}` (the WindowHost presents it); `{effect: flush}` (the outbox flushes: outbox.json; at launch, on foreground and on `online`, never in the background or stopped); `{effect: sources, live: true|false}` (live subscriptions of the open screen's bindings run only in the foreground; background pauses them); `{effect: automations, on: true|false}` (an app's automations run only while its instance runs, in the foreground and background: Spezl's rule; on at launch, off at terminate); `{effect: persist, screen, params}` (on background and terminate the instance stores its screen, which the next plain launch restores); `{effect: refused, reason}` (a link or tap that cannot open: the host shows the failed-action toast `copy.refused`; a running instance stays where it was, a cold launch opens its plain-launch screen after it). An event that does not apply (foreground while foreground, background while stopped, launch while running, …) is IGNORED: same state, no effects. A cold launch's effects: `automations on`, `open`, `sources live`, `flush`; going to the background: `persist`, `sources off`; foreground: `sources live`, `flush`; terminate: `persist` (if it ran), `sources off` (from the foreground), `automations off`. A deepLink or tap while RUNNING opens its target; from the background it also foregrounds (`open`, then `sources live`, `flush`).",
|
|
3
|
+
"copy": { "refused": "Couldn't open the link" },
|
|
4
|
+
"package": {
|
|
5
|
+
"id": "acme.album",
|
|
6
|
+
"screens": [
|
|
7
|
+
{ "template": "albums", "title": "Albums", "icon": "ui-image" },
|
|
8
|
+
{ "template": "pages", "title": "Pages", "icon": "ui-file", "exposed": { "description": "An album's pages", "params": { "album": { "required": true } } } },
|
|
9
|
+
{ "template": "page", "title": "Page", "nav": false, "params": { "zoom": "fit" } }
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"cases": [
|
|
13
|
+
{
|
|
14
|
+
"name": "a plain cold launch opens the first navigation screen",
|
|
15
|
+
"lastScreen": null,
|
|
16
|
+
"steps": [
|
|
17
|
+
{
|
|
18
|
+
"event": { "type": "launch" },
|
|
19
|
+
"state": { "phase": "foreground", "screen": "albums", "params": {} },
|
|
20
|
+
"effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "a plain launch restores the persisted screen",
|
|
26
|
+
"lastScreen": { "screen": "page", "params": { "id": "p1" } },
|
|
27
|
+
"steps": [
|
|
28
|
+
{
|
|
29
|
+
"event": { "type": "launch" },
|
|
30
|
+
"state": { "phase": "foreground", "screen": "page", "params": { "id": "p1", "zoom": "fit" } },
|
|
31
|
+
"effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "page", "params": { "id": "p1", "zoom": "fit" } }, { "effect": "sources", "live": true }, { "effect": "flush" }]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "a persisted screen the package no longer has falls back",
|
|
37
|
+
"lastScreen": { "screen": "gone", "params": {} },
|
|
38
|
+
"steps": [
|
|
39
|
+
{
|
|
40
|
+
"event": { "type": "launch" },
|
|
41
|
+
"state": { "phase": "foreground", "screen": "albums", "params": {} },
|
|
42
|
+
"effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "background, foreground, terminate",
|
|
48
|
+
"lastScreen": null,
|
|
49
|
+
"steps": [
|
|
50
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
51
|
+
{ "event": { "type": "background" }, "state": { "phase": "background", "screen": "albums", "params": {} }, "effects": [{ "effect": "persist", "screen": "albums", "params": {} }, { "effect": "sources", "live": false }] },
|
|
52
|
+
{ "event": { "type": "online" }, "state": { "phase": "background", "screen": "albums", "params": {} }, "effects": [] },
|
|
53
|
+
{ "event": { "type": "background" }, "state": { "phase": "background", "screen": "albums", "params": {} }, "effects": [] },
|
|
54
|
+
{ "event": { "type": "foreground" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
55
|
+
{ "event": { "type": "online" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "flush" }] },
|
|
56
|
+
{ "event": { "type": "terminate" }, "state": { "phase": "stopped", "screen": null, "params": null }, "effects": [{ "effect": "persist", "screen": "albums", "params": {} }, { "effect": "sources", "live": false }, { "effect": "automations", "on": false }] },
|
|
57
|
+
{ "event": { "type": "foreground" }, "state": { "phase": "stopped", "screen": null, "params": null }, "effects": [] }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "terminate from the background",
|
|
62
|
+
"lastScreen": null,
|
|
63
|
+
"steps": [
|
|
64
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
65
|
+
{ "event": { "type": "background" }, "state": { "phase": "background", "screen": "albums", "params": {} }, "effects": [{ "effect": "persist", "screen": "albums", "params": {} }, { "effect": "sources", "live": false }] },
|
|
66
|
+
{ "event": { "type": "terminate" }, "state": { "phase": "stopped", "screen": null, "params": null }, "effects": [{ "effect": "persist", "screen": "albums", "params": {} }, { "effect": "automations", "on": false }] }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "a deep link cold-launches into an exposed screen with its declared params",
|
|
71
|
+
"lastScreen": null,
|
|
72
|
+
"steps": [
|
|
73
|
+
{
|
|
74
|
+
"event": { "type": "deepLink", "screen": "pages", "params": { "album": "a1", "secret": "x" } },
|
|
75
|
+
"state": { "phase": "foreground", "screen": "pages", "params": { "album": "a1" } },
|
|
76
|
+
"effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "pages", "params": { "album": "a1" } }, { "effect": "sources", "live": true }, { "effect": "flush" }]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "a deep link to a screen that is not exposed is refused, the cold launch opens the plain screen",
|
|
82
|
+
"lastScreen": null,
|
|
83
|
+
"steps": [
|
|
84
|
+
{
|
|
85
|
+
"event": { "type": "deepLink", "screen": "page", "params": { "id": "p1" } },
|
|
86
|
+
"state": { "phase": "foreground", "screen": "albums", "params": {} },
|
|
87
|
+
"effects": [{ "effect": "refused", "reason": "page is not exposed" }, { "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "a deep link missing a required param is refused while running",
|
|
93
|
+
"lastScreen": null,
|
|
94
|
+
"steps": [
|
|
95
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
96
|
+
{ "event": { "type": "deepLink", "screen": "pages", "params": {} }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "refused", "reason": "pages needs album" }] }
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "a notification tap from the background opens a detail screen and foregrounds",
|
|
101
|
+
"lastScreen": null,
|
|
102
|
+
"steps": [
|
|
103
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
104
|
+
{ "event": { "type": "background" }, "state": { "phase": "background", "screen": "albums", "params": {} }, "effects": [{ "effect": "persist", "screen": "albums", "params": {} }, { "effect": "sources", "live": false }] },
|
|
105
|
+
{ "event": { "type": "notificationTap", "app": "acme.album", "screen": "page", "params": { "id": "p2" } }, "state": { "phase": "foreground", "screen": "page", "params": { "id": "p2", "zoom": "fit" } }, "effects": [{ "effect": "open", "screen": "page", "params": { "id": "p2", "zoom": "fit" } }, { "effect": "sources", "live": true }, { "effect": "flush" }] }
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "a notification tap of another app is refused",
|
|
110
|
+
"lastScreen": null,
|
|
111
|
+
"steps": [
|
|
112
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
113
|
+
{ "event": { "type": "notificationTap", "app": "acme.other", "screen": "page", "params": {} }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "refused", "reason": "otherApp" }] }
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "a second launch while running is ignored",
|
|
118
|
+
"lastScreen": null,
|
|
119
|
+
"steps": [
|
|
120
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [{ "effect": "automations", "on": true }, { "effect": "open", "screen": "albums", "params": {} }, { "effect": "sources", "live": true }, { "effect": "flush" }] },
|
|
121
|
+
{ "event": { "type": "launch" }, "state": { "phase": "foreground", "screen": "albums", "params": {} }, "effects": [] }
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.1, §14): THE ENGINE'S OUTBOX IS O1's, NOT A COPY. The entry, the writes, the overlay, the replay, FIFO order, the outcomes, the prune and the pending label are `packages/domain-contract/fixtures/app-records.json` `outbox` (VAPP-235) exactly as written (`$ref`; the TS reference re-exports `@exp/domain-contract/app-records` `outbox*`, the Rust Engine replays the referenced section). What the Engine adds is WHEN it applies (`modeCases`, `outboxMode`): a host with a `sync` provider (providers.json: Exponential's SyncedRecordStore, Spezl's P2P space) queues every app write as an entry and flushes on lifecycle.json's `flush` effects; a host WITHOUT one (`local` identity, a SqliteRecordStore: the softcoded app, eDE Local desktops, exported apps) has no outbox: a write applies to the store at once, in one transaction per call (a batch stays all or nothing), the store's own `writeId` dedupe still applies, and the pending label never shows. The record store is the authority in local mode; there is nothing to reconcile. THE ACCOUNT'S QUEUE (`syncedCases`, `SyncedRecordStore`, VAPP-272): Exponential's hosts keep ONE outbox per account, across its apps, persisted in the host's own store through an `OutboxLog` (`entries`, `push`, `ack`, `remove`: desktop sync sqlite, GRDB, Room), so the queue itself lives in the Engine once and each host only fills in the log and the transport. A case starts from the `log`, then runs `steps` in order: `submit` a host call's `entry` (`online` = the device's connection), `flush`, `rows` (the synced rows of an `app`, which prune its acked entries) or `prune` at `now`. `transport` scripts each write id's replays in order (`{ok: true, answer}` | `{transport: true}` | `{code, message?, invalid?}`; none left = no network). SUBMIT persists the entry first, then: with nothing else pending on a connected device it replays at once and answers its OWN entry's first outcome (`{done: <the server's answer>}`, `{refused: {message, invalid}}`, or `{queued: true}` on a retry or a drop); otherwise `{queued: true}` with nothing sent, and the host starts the flush. FLUSH: one at a time per account, FIFO across the account's apps, and a retry stops it. A refusal nobody awaits goes to the listener (`refused`: the failed-action toast); an awaited one does not. `sent` = the replays in order, `refused` = `{writeId, verb, message}` of the unawaited ones, `logAfter` = the entries left as `{writeId, state}`. The entries are re-read from the log only when the store is created (a restart).",
|
|
3
|
+
"$ref": "../../domain-contract/fixtures/app-records.json#/outbox",
|
|
4
|
+
"modeCases": [
|
|
5
|
+
{ "name": "a sync provider queues", "sync": true, "expected": "queue" },
|
|
6
|
+
{ "name": "no sync provider writes through", "sync": false, "expected": "direct" }
|
|
7
|
+
],
|
|
8
|
+
"syncedNow": "2026-11-07T10:00:00.000Z",
|
|
9
|
+
"syncedCases": [
|
|
10
|
+
{"name": "alone and online: the call answers the server's own answer", "log": [], "transport": {"w1": [{"ok": true, "answer": {"id": "r-w1"}}]}, "steps": [{"do": "submit", "entry": {"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": true, "expected": {"done": {"id": "r-w1"}}}], "sent": ["w1"], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}]},
|
|
11
|
+
{"name": "offline: queued, nothing sent, then the flush replays it", "log": [], "transport": {"w1": [{"ok": true, "answer": {"id": "r-w1"}}]}, "steps": [{"do": "submit", "entry": {"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": false, "expected": {"queued": true}}, {"do": "flush"}], "sent": ["w1"], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}]},
|
|
12
|
+
{"name": "behind a pending entry: queued, and FIFO across the account's apps", "log": [{"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}], "transport": {"w1": [{"ok": true}], "w2": [{"ok": true}]}, "steps": [{"do": "submit", "entry": {"writeId": "w2", "appId": "b", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": true, "expected": {"queued": true}}, {"do": "flush"}], "sent": ["w1", "w2"], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}, {"writeId": "w2", "state": "acked"}]},
|
|
13
|
+
{"name": "a transport failure stops the flush and nothing overtakes it; the next flush goes on", "log": [{"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, {"writeId": "w2", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}], "transport": {"w1": [{"transport": true}, {"code": "INTERNAL_SERVER_ERROR"}, {"ok": true}], "w2": [{"ok": true}]}, "steps": [{"do": "flush"}, {"do": "flush"}, {"do": "flush"}], "sent": ["w1", "w1", "w1", "w2"], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}, {"writeId": "w2", "state": "acked"}]},
|
|
14
|
+
{"name": "a retry on the call's own entry answers from the entry and keeps it pending", "log": [], "transport": {"w1": [{"transport": true}]}, "steps": [{"do": "submit", "entry": {"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": true, "expected": {"queued": true}}], "sent": ["w1"], "refused": [], "logAfter": [{"writeId": "w1", "state": "pending"}]},
|
|
15
|
+
{"name": "an awaited refusal fails the call, leaves the log and raises no toast", "log": [], "transport": {"w1": [{"code": "BAD_REQUEST", "message": "title is required", "invalid": true}]}, "steps": [{"do": "submit", "entry": {"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": true, "expected": {"refused": {"message": "title is required", "invalid": true}}}], "sent": ["w1"], "refused": [], "logAfter": []},
|
|
16
|
+
{"name": "a later refusal goes to the listener and the flush goes on", "log": [{"writeId": "w1", "appId": "a", "call": "delete", "writes": [{"op": "delete", "id": "x"}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, {"writeId": "w2", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}], "transport": {"w1": [{"code": "FORBIDDEN", "message": "no"}], "w2": [{"ok": true}]}, "steps": [{"do": "flush"}], "sent": ["w1", "w2"], "refused": [{"writeId": "w1", "verb": "delete", "message": "no"}], "logAfter": [{"writeId": "w2", "state": "acked"}]},
|
|
17
|
+
{"name": "NOT_FOUND on a single update drops silently (delete wins), and the call answers from its entry", "log": [], "transport": {"w1": [{"code": "NOT_FOUND"}]}, "steps": [{"do": "submit", "entry": {"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "update", "id": "gone", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}, "online": true, "expected": {"queued": true}}], "sent": ["w1"], "refused": [], "logAfter": []},
|
|
18
|
+
{"name": "a restart reads the log: acked entries stay, pending ones replay", "log": [{"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "acked", "ackedAt": "2026-11-07T09:59:50.000Z"}, {"writeId": "w2", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "pending"}], "transport": {"w2": [{"ok": true}]}, "steps": [{"do": "flush"}], "sent": ["w2"], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}, {"writeId": "w2", "state": "acked"}]},
|
|
19
|
+
{"name": "rows that reflect an acked entry prune it; rows that do not keep it", "log": [{"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "acked", "ackedAt": "2026-11-07T09:59:50.000Z"}, {"writeId": "w2", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "acked", "ackedAt": "2026-11-07T09:59:50.000Z"}], "transport": {}, "steps": [{"do": "rows", "app": "a", "rows": [{"id": "r-w1", "kind": "task", "data": {"title": "w1"}, "createdBy": "me", "authorId": "me", "createdAt": "2026-11-07T09:00:00.000Z", "updatedAt": "2026-11-07T09:00:00.000Z"}]}], "sent": [], "refused": [], "logAfter": [{"writeId": "w2", "state": "acked"}]},
|
|
20
|
+
{"name": "an app whose rows are unknown keeps its acked entries; past the grace with rows known they leave", "log": [{"writeId": "w1", "appId": "a", "call": "upsert", "writes": [{"op": "create", "id": "r-w1", "kind": "task", "data": {"title": "w1"}}], "at": "2026-11-07T09:00:00.000Z", "state": "acked", "ackedAt": "2026-11-07T09:00:00.000Z"}, {"writeId": "w2", "appId": "b", "call": "upsert", "writes": [{"op": "create", "id": "r-w2", "kind": "task", "data": {"title": "w2"}}], "at": "2026-11-07T09:00:00.000Z", "state": "acked", "ackedAt": "2026-11-07T09:00:00.000Z"}], "transport": {}, "steps": [{"do": "rows", "app": "b", "rows": []}, {"do": "prune"}], "sent": [], "refused": [], "logAfter": [{"writeId": "w1", "state": "acked"}]}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.1): WHAT A HOST HANDS THE ENGINE, the `HostConfig`. Each key of `providers` is `required` (the Engine refuses to start without it: `missing`) or optional (absent = its `default`); `list: true` = zero or more. There is NO telemetry and NO update provider: `forbidden` keys are refused by name (`forbidden`), so an exported runner can never grow an analytics, crash-report or over-the-air channel through its config (a new package version = a rebundle, decided 2026-11-05); any other unknown key is `unknown`. `network` is one of `networkPolicies`; `identity.kind` one of `identityKinds`. A capability is mounted under its name (capabilities.json), each name once (`duplicateCapability`); an agent provider (Agents' `AgentProvider`: `acp`, `localModel`, `remoteSession`) once per id (`duplicateAgent`). `validateHostConfig(config)` -> the issues `{key, code}` in `providers` order, then forbidden and unknown keys sorted. HOSTS (`hosts`): what each host kind provides, informative and checked against the validator (each must pass). IDENTITY (`identityCases`, `engineViewer`): `member` = the signed-in member with their team role and app access (Exponential); `local` = ONE person on one device and no account (the softcoded app, eDE Local desktops, exported apps): the viewer = `{id: the device uuid, role: owner, access: write}`, so genUI's `recordRefusal` allows every op of every kind policy (`member`, `author`, `owners` all collapse to \"me\"), and records the instance writes carry the device uuid as author; `peer` = a P2P key (Spezl), judged like `member` with the key as id and role `member`. NETWORK (`networkCases`, `networkAllows`): `off` = nothing leaves the device (upstreams, `http.fetch` and a sync transport are refused `offline`); `upstreamsOnly` = the package's declared `upstreams` and the sync transport, never `http.fetch`; `any` = all three (an `http.fetch` still needs its manifest origin and consent: capabilities.json). A refusal is `{allowed: false, reason}`.",
|
|
3
|
+
"providers": {
|
|
4
|
+
"host": { "required": true, "doc": "HostInfo: host kind, platform, form factor, locale, Engine version" },
|
|
5
|
+
"storage": { "required": true, "doc": "RecordStore: query, write (writeId), history, subscribe (MemoryRecordStore, SqliteRecordStore = elocal's app-records table, SyncedRecordStore)" },
|
|
6
|
+
"blobs": { "required": true, "doc": "BlobStore: put/get record files (FsBlobStore, the server's attachments)" },
|
|
7
|
+
"identity": { "required": true, "doc": "Identity: the viewer (member, local or peer)" },
|
|
8
|
+
"vault": { "required": true, "doc": "emcp Vault: grants, upstream tokens (Keychain, Keystore, libsecret, sealed server column)" },
|
|
9
|
+
"window": { "required": true, "doc": "WindowHost: open a screen (window, nav stack, sheet), report lifecycle events" },
|
|
10
|
+
"clock": { "required": false, "default": "system", "doc": "Clock: now (tests pin it)" },
|
|
11
|
+
"zone": { "required": false, "default": "device", "doc": "HostZone: the IANA zone of today/now/month (the team's in Exponential)" },
|
|
12
|
+
"network": { "required": false, "default": "upstreamsOnly", "doc": "NetworkPolicy" },
|
|
13
|
+
"sync": { "required": false, "default": null, "doc": "SyncTransport: pull deltas, push outbox entries (Exponential: Electric in, tRPC out; Spezl: P2P); absent = writes apply locally, no outbox" },
|
|
14
|
+
"agents": { "required": false, "default": [], "list": true, "doc": "AgentProvider (Agents): acp, localModel, remoteSession; each gets its tools only as a hub grant" },
|
|
15
|
+
"capabilities": { "required": false, "default": [], "list": true, "doc": "Capability: a native tool provider on the in-process hub (capabilities.json)" },
|
|
16
|
+
"consent": { "required": false, "default": "sheet", "doc": "ConsentUi: the Engine's own genUI sheet unless the host paints it (consent.json)" }
|
|
17
|
+
},
|
|
18
|
+
"forbidden": ["analytics", "crashReporter", "telemetry", "updateChannel", "updates"],
|
|
19
|
+
"networkPolicies": ["off", "upstreamsOnly", "any"],
|
|
20
|
+
"identityKinds": ["member", "local", "peer"],
|
|
21
|
+
"hosts": {
|
|
22
|
+
"exponential": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "member" }, "vault": {}, "window": {}, "zone": {}, "network": "any", "sync": {}, "agents": [{ "id": "acp" }, { "id": "remoteSession" }], "capabilities": [{ "name": "files" }, { "name": "share" }, { "name": "notifications" }] },
|
|
23
|
+
"softcoded": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {}, "network": "upstreamsOnly", "agents": [{ "id": "localModel" }] },
|
|
24
|
+
"ede": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {}, "network": "any", "agents": [{ "id": "acp" }], "capabilities": [{ "name": "files" }, { "name": "notifications" }, { "name": "shell" }, { "name": "windows" }, { "name": "focus" }, { "name": "launch" }] },
|
|
25
|
+
"spezl": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "peer" }, "vault": {}, "window": {}, "network": "any", "sync": {}, "agents": [{ "id": "acp" }] },
|
|
26
|
+
"exported": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {}, "network": "off", "capabilities": [{ "name": "photos" }, { "name": "print" }, { "name": "share" }] }
|
|
27
|
+
},
|
|
28
|
+
"validateCases": [
|
|
29
|
+
{ "name": "the minimum", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {} }, "expected": [] },
|
|
30
|
+
{ "name": "every required key missing", "config": {}, "expected": [{ "key": "host", "code": "missing" }, { "key": "storage", "code": "missing" }, { "key": "blobs", "code": "missing" }, { "key": "identity", "code": "missing" }, { "key": "vault", "code": "missing" }, { "key": "window", "code": "missing" }] },
|
|
31
|
+
{ "name": "no telemetry, no update channel", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {}, "updates": {}, "telemetry": {}, "theme": {} }, "expected": [{ "key": "telemetry", "code": "forbidden" }, { "key": "theme", "code": "unknown" }, { "key": "updates", "code": "forbidden" }] },
|
|
32
|
+
{ "name": "a bad network policy and identity kind", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "guest" }, "vault": {}, "window": {}, "network": "lan" }, "expected": [{ "key": "identity", "code": "invalid" }, { "key": "network", "code": "invalid" }] },
|
|
33
|
+
{ "name": "a capability or agent twice", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "member" }, "vault": {}, "window": {}, "agents": [{ "id": "acp" }, { "id": "acp" }], "capabilities": [{ "name": "photos" }, { "name": "photos" }] }, "expected": [{ "key": "agents", "code": "duplicateAgent" }, { "key": "capabilities", "code": "duplicateCapability" }] },
|
|
34
|
+
{ "name": "a capability under a reserved or invalid name", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "member" }, "vault": {}, "window": {}, "capabilities": [{ "name": "records" }, { "name": "Bad_Name" }] }, "expected": [{ "key": "capabilities", "code": "invalidCapability" }] },
|
|
35
|
+
{ "name": "an unknown agent provider", "config": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "member" }, "vault": {}, "window": {}, "agents": [{ "id": "openclaw" }] }, "expected": [{ "key": "agents", "code": "invalid" }] }
|
|
36
|
+
],
|
|
37
|
+
"agentProviders": ["acp", "localModel", "remoteSession"],
|
|
38
|
+
"identityCases": [
|
|
39
|
+
{ "name": "local: an author-only update of someone else's record", "identity": { "kind": "local", "deviceId": "d-1" }, "policy": { "read": "author", "create": "owners", "update": "author", "delete": "owners" }, "op": "update", "row": { "authorId": "u-imported" }, "expected": { "viewer": { "id": "d-1", "role": "owner", "access": "write" }, "refusal": null } },
|
|
40
|
+
{ "name": "local: an owners-only delete", "identity": { "kind": "local", "deviceId": "d-1" }, "policy": { "read": "member", "create": "member", "update": "author", "delete": "owners" }, "op": "delete", "row": { "authorId": "d-1" }, "expected": { "viewer": { "id": "d-1", "role": "owner", "access": "write" }, "refusal": null } },
|
|
41
|
+
{ "name": "member: the policy applies", "identity": { "kind": "member", "userId": "u-1", "role": "member", "access": "write" }, "policy": { "read": "member", "create": "member", "update": "author", "delete": "author" }, "op": "update", "row": { "authorId": "u-2" }, "expected": { "viewer": { "id": "u-1", "role": "member", "access": "write" }, "refusal": "Only the record's author or a team owner can change it" } },
|
|
42
|
+
{ "name": "peer: a key judged as a member", "identity": { "kind": "peer", "key": "pk-9" }, "policy": { "read": "member", "create": "member", "update": "author", "delete": "author" }, "op": "update", "row": { "authorId": "pk-9" }, "expected": { "viewer": { "id": "pk-9", "role": "member", "access": "write" }, "refusal": null } }
|
|
43
|
+
],
|
|
44
|
+
"networkCases": [
|
|
45
|
+
{ "policy": "off", "target": "upstream", "declared": true, "expected": { "allowed": false, "reason": "offline" } },
|
|
46
|
+
{ "policy": "off", "target": "sync", "declared": true, "expected": { "allowed": false, "reason": "offline" } },
|
|
47
|
+
{ "policy": "off", "target": "http", "declared": true, "expected": { "allowed": false, "reason": "offline" } },
|
|
48
|
+
{ "policy": "upstreamsOnly", "target": "upstream", "declared": true, "expected": { "allowed": true, "reason": null } },
|
|
49
|
+
{ "policy": "upstreamsOnly", "target": "upstream", "declared": false, "expected": { "allowed": false, "reason": "notDeclared" } },
|
|
50
|
+
{ "policy": "upstreamsOnly", "target": "sync", "declared": true, "expected": { "allowed": true, "reason": null } },
|
|
51
|
+
{ "policy": "upstreamsOnly", "target": "http", "declared": true, "expected": { "allowed": false, "reason": "network" } },
|
|
52
|
+
{ "policy": "any", "target": "http", "declared": true, "expected": { "allowed": true, "reason": null } },
|
|
53
|
+
{ "policy": "any", "target": "upstream", "declared": false, "expected": { "allowed": false, "reason": "notDeclared" } }
|
|
54
|
+
]
|
|
55
|
+
}
|