@exponential_app/engine 0.1.0 → 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/README.md +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/chunks/{index-ax4aay30.js → index-3nf9f7pv.js} +2 -2
- package/dist/chunks/{index-ans80t68.js → index-3rgr16n2.js} +67 -6
- package/dist/chunks/{index-e49v2qpf.js.map → index-3rgr16n2.js.map} +1 -1
- package/dist/chunks/{index-n0acffvs.js → index-79swfbsm.js} +3 -3
- package/dist/chunks/{index-nrr8x2kq.js → index-knntt4cb.js} +2 -2
- package/dist/chunks/{index-e49v2qpf.js → index-pqpnz4ns.js} +38 -2
- package/dist/chunks/{index-ans80t68.js.map → index-pqpnz4ns.js.map} +1 -1
- package/dist/chunks/{index-t89cvabm.js → index-q4zws02e.js} +2 -2
- package/dist/chunks/{index-zw0575tv.js → index-sk8b0dh4.js} +4 -4
- package/dist/chunks/{index-zw0575tv.js.map → index-sk8b0dh4.js.map} +1 -1
- package/dist/chunks/{index-md3hz6r2.js → index-tvsykgcv.js} +3 -3
- package/dist/chunks/{index-md3hz6r2.js.map → index-tvsykgcv.js.map} +2 -2
- package/dist/chunks/{index-4ntgtkmb.js → index-v0m7r4y3.js} +90 -4
- package/dist/chunks/{index-4ntgtkmb.js.map → index-v0m7r4y3.js.map} +3 -3
- package/dist/consent.js +2 -2
- package/dist/index.js +136 -8
- package/dist/index.js.map +5 -4
- package/dist/outbox.js +1 -1
- package/dist/pdf.js +1 -1
- package/dist/providers.js +2 -2
- package/dist/runner.js +2 -2
- package/dist/tools.js +2 -2
- package/dist/types/domain-contract/src/apps.d.ts +18 -0
- package/dist/types/eengine/fixtures/providers.json +1 -1
- package/dist/types/eengine/src/capabilities.d.ts +2 -0
- package/dist/types/eengine/src/index.d.ts +1 -0
- package/dist/types/eengine/src/instance.d.ts +9 -0
- package/dist/types/eengine/src/synced.d.ts +59 -0
- package/fixtures/capabilities.json +65 -4
- package/fixtures/outbox.json +15 -1
- package/fixtures/providers.json +1 -1
- package/package.json +1 -1
- /package/dist/chunks/{index-ax4aay30.js.map → index-3nf9f7pv.js.map} +0 -0
- /package/dist/chunks/{index-n0acffvs.js.map → index-79swfbsm.js.map} +0 -0
- /package/dist/chunks/{index-nrr8x2kq.js.map → index-knntt4cb.js.map} +0 -0
- /package/dist/chunks/{index-t89cvabm.js.map → index-q4zws02e.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.1): THE CAPABILITY CATALOG AND THE ONE BROKER. A CAPABILITY is a native tool provider the HOST implements per platform (E5) and mounts on the Engine's in-process MCP Proxy hub (packages/emcp hub.json) under its `name` (a hub server name: `hubServerNameValid`, never a `reservedNames` entry, so no capability can shadow the package's own functions or the Engine's internals). It declares SCOPES (`scopes`): a scope id `<capability>.<scope>` (`scopePattern`) is what a package's capability manifest names (package format 5 `capabilities: [{id, why, origins?}]`) and what a grant is kept for; each scope lists its TOOLS and its CONSENT: `ask` = the Engine's consent sheet once, the answer persisted (consent.json); `picker` = the platform's own picker or sheet IS the consent, every call, so nothing persists and the call needs the app in the foreground; `hostOnly: true` = a scope of the host itself (eDE's `shell.edit`, editing its ede-ui documents), NEVER in a manifest (the validator refuses it) and never granted to an app. `origins: true` = the manifest entry must list `origins` (exact `https://host[:port]` origins) and a call's `url` must be on one. NAMES: an agent calls `<capability>__<tool>` (exposes.json's qualified name, server = capability); app UI calls the function `<capability>.<tool>` (it must be in the package's `functions` allowlist like any other function); BOTH parse to the same `{capability, tool}` (`parseCases`), anything else is `unknown`. THE BROKER (`decideCases`, `brokerDecide`) is ONE function for UI and agent calls: the FIRST of: `unknown` (no such capability or tool: the agent answer is MCP's unknown-tool error -32602, never a hint) · `denied/hostOnly` · `denied/notDeclared` (the manifest is the CEILING: no scope declared = no call, no sheet) · `unavailable` (this host does not provide the capability, e.g. `camera` on a desktop without one) · `denied/originNotDeclared` (an `origins` scope whose `url` argument is missing or off the list) · for `picker`: `denied/needsForeground` in the background, else `call` · for `ask`: the stored grant `granted` = `call`, `denied` = `denied/consentDenied`, undecided = `ask` in the foreground (the Engine shows the consent sheet; consent.json `consentAnswer` turns the answer into the call's verdict) and `denied/consentNeeded` in the background (never a sheet behind the user's back). An agent's tools/list (`listCases`, `agentToolList`) = every tool of a scope the manifest declares and the host provides, `hostOnly` excluded, sorted by qualified name; a denied scope stays listed (the call answers `consentDenied`, so the agent can tell the person). THE AUDIT (`auditCases`, `brokerAuditRow`): every call, refused ones included, writes ONE row in audit.json's shape to the host's sink: `appId` = the package id, `clientId` = `ui` for app UI or the run id for an agent, `tool` = the qualified name `<capability>__<tool>` for both, `status` = `denied` for every refusal and `unknown`/`unavailable`, else audit.json's status of the call's outcome (`ok`, `isError` -> failed, `invalidParams` -> invalid, `rpcError`/`unreachable`/`timeout` -> upstream), `error` = the refusal's reason (`unknownTool` for unknown, `unavailable`), null on ok and failed; `argsHash` = audit.json's hash of the arguments as received. No hub tool writes the package, the grants or the host config: a grant changes only in consent.json's `grantWrite`. CAPABILITIES V1 (VAPP-265, E5): the TOOLS (`tools`, sorted by name) lock every tool's name, its app-facing `inputSchema`/`outputSchema` (what app UI and agents see: an agent's tools/list lists them as `<capability>__<tool>` with the same description and schemas) and, for `runs: native`, the `nativeOutputSchema` a host's native provider answers; `runs: engine` = the Engine answers it itself (`print_pdf`, no native call; the capability must still be provided). A host's native capability implements EVERY native tool of its capability (one it cannot serve answers isError `unavailable`). THE BRIDGE (`bridgeCases`, `bridgeCall`) runs between the broker's `call` verdict and the native, the same for UI and agents: (1) VALIDATE the arguments (`validateCases`) against `inputSchema` with the subset `type` (one or a list; `integer` = an integral number) · `properties` · `required` · `additionalProperties: false` · `minimum`/`maximum` · `minLength`/`maxLength` (code points) · `minItems`/`maxItems` · `items` · `enum` · `format` (`date-time` = RFC 3339 with a zone; `token` = 1 to 64 of `[A-Za-z0-9._-]`; `uri` = an absolute http(s) URL; `base64` = the standard alphabet, padded) · `dependentRequired` · `anyOf`/`oneOf` of `required` sets; checked in that order per value: type, then for an object required (listed order), unknown keys (sorted), each property (schema order), dependentRequired (its key order; the path is the MISSING key's), anyOf, oneOf; for an array minItems, maxItems, then each item; the FIRST violation answers isError `invalidParams: <JSON pointer, / for the root> <keyword>` (audit outcome `invalidParams`) and the native never runs; a missing top-level property with a `default` gets it. (2) MAP to the native arguments: photos.*, files_open = the validated arguments; files_save = `{name, mimeType, data}`; share_send = the given `title`/`text`/`url` plus `file: {name, mimeType, data}` for a `fileId`; print_print = `{name, mimeType, data}` (`name` default `document.<pdf|png|jpg>`); notifications_post = `{id: \"<app>/<id>\", title, body|null, at|null, tap}` where `tap` = lifecycle.json's `{type: notificationTap, app, screen, params ({} default)}` or null without a `screen` (a `screen` must be one of the package's screens, else `invalidParams: /screen enum`) and an absent `id` is generated (`n-` + 32 lowercase hex), so a host hands the tap back as the instance's event and ids never collide across apps; notifications_cancel = `{id: \"<app>/<id>\"}`. A `fileId` resolves through the host's BlobStore (`v1.context` = the app and its screens; missing = isError `notFound: <fileId>`), `data` = its bytes in base64; `text` = its UTF-8 bytes. `mimeType` = the given one, else `v1.mimeByExtension` of the name's last `.ext` (case-insensitive), else sniffed from the bytes (`%PDF-` application/pdf, FF D8 FF image/jpeg, the PNG signature image/png, `GIF8` image/gif), else text/plain for a `text` and application/octet-stream; print_print takes only a sniffed PDF, PNG or JPEG (else isError `unsupportedFile`). (3) CHECK the native's result: an `isError` result passes through as is (audit `failed`); otherwise its `structuredContent` must match `nativeOutputSchema` (else isError `badResult: <pointer> <keyword>`, nothing stored). (4) STORE every object in it that carries `data` (depth first, in order): BlobStore `put(app, name, mimeType, bytes)` (ids in the cases: `blob-1`, `blob-2`, …), the object loses `data` and gains `size` (bytes) and `fileId`; notifications_post answers `{id}` (the app's own id). Every answer is an MCP call result `{content: [{type: text, text: <the structuredContent as JSON>}], structuredContent}`, errors `{content: [{type: text, text: <reason>}], isError: true}`. THE PDF (`pdf`, `pdfCases`, `print_pdf`): ONE layout both twins write byte for byte (sha256 + size locked): A4 (`pdf.page` points), margins 56, content width 483, the cursor y from 786 down to 56; fonts F1 Helvetica and F2 Helvetica-Bold (Type1, WinAnsiEncoding; `pdf.widths` = their widths per WinAnsi code in 1/1000 em, `pdf.winAnsi` = the code points 0x80-0x9F map to; ASCII 0x20-0x7E and 0xA0-0xFF are themselves; a tab is a space, other controls drop, anything else is `?`). A text's width at size s = sum(widths) × s / 1000, compared exactly (sum × s ≤ 483000). Lines: a paragraph per `\\n`, words split on runs of spaces, greedy, a word wider than the line broken at the longest fitting prefix (one character at least); an empty paragraph is one empty line. A line at size s and leading L: if the page has content and y − L < 56, a new page; then `BT /F<n> <s> Tf 56 <y − s> Td (<text>) Tj ET` (an empty line writes nothing) and y −= L. heading: y −= 12 first unless the page is empty, F2 18/24, then y −= 4. text: F1 11/15, then y −= 6. image (a JPEG: SOF0-SOF3 width, height and 1/3/4 components = DeviceGray/RGB/CMYK, DCTDecode as is; a PNG: 8-bit, colour type 0 or 2, not interlaced, its IDATs concatenated with FlateDecode + DecodeParms Predictor 15; anything else isError `unsupportedImage: <fileId>`, a missing file `notFound: <fileId>`): scale = min(483/w, 730/h, 1), if the page has content and y − h·scale < 56 a new page, `q <w·scale> 0 0 <h·scale> 56 <y − h·scale> cm /Im<k> Do Q`, y −= h·scale; its caption: y −= 4, F1 9/12 lines; then y −= 6. pageBreak: a new page unless the page is empty. The last page is dropped if it is empty and not the only one. Numbers print rounded to 2 decimals, shortest form (`12`, `12.5`). Text strings escape `(`, `)` and `\\` and write bytes ≥ 0x80 as 3-digit octal. The file: `%PDF-1.4\\n%` E2 E3 CF D3 `\\n`; objects `<n> 0 obj\\n<body>\\nendobj\\n`: 1 the Catalog, 2 Pages (Kids, Count), 3 F1, 4 F2, 5 Info (`/Title <FEFF…>` UTF-16BE uppercase hex, `/Producer (Exponential Engine)`), then one XObject per image file in first-use order (`/Im1`…, a file used twice is one object), then per page its Page (MediaBox, `/Resources << /Font << /F1 3 0 R /F2 4 0 R >> /XObject << … >> >>`, the XObject dict only with the page's images in number order) and its content; a stream body is `<< … /Length <n> >>\\nstream\\n<data>\\nendstream` (content data = the page's operators joined by `\\n`); then `xref\\n0 <size>\\n0000000000 65535 f \\n`, one `<10-digit offset> 00000 n \\n` per object, `trailer\\n<< /Size <size> /Root 1 0 R /Info 5 0 R >>\\nstartxref\\n<offset>\\n%%EOF\\n`. The file is stored as `name` (default the title with `/\\:*?\"<>|` and controls replaced by `-`, + `.pdf`), application/pdf; the answer `{file, pages}`.",
|
|
2
|
+
"$comment": "VAPP-260 (Engine E0, epic VAPP-259, docs/spec/apps.md §13.1): THE CAPABILITY CATALOG AND THE ONE BROKER. A CAPABILITY is a native tool provider the HOST implements per platform (E5) and mounts on the Engine's in-process MCP Proxy hub (packages/emcp hub.json) under its `name` (a hub server name: `hubServerNameValid`, never a `reservedNames` entry, so no capability can shadow the package's own functions or the Engine's internals). It declares SCOPES (`scopes`): a scope id `<capability>.<scope>` (`scopePattern`) is what a package's capability manifest names (package format 5 `capabilities: [{id, why, origins?}]`) and what a grant is kept for; each scope lists its TOOLS and its CONSENT: `ask` = the Engine's consent sheet once, the answer persisted (consent.json); `picker` = the platform's own picker or sheet IS the consent, every call, so nothing persists and the call needs the app in the foreground; `hostOnly: true` = a scope of the host itself (eDE's `shell.edit`, editing its ede-ui documents), NEVER in a manifest (the validator refuses it) and never granted to an app. `origins: true` = the manifest entry must list `origins` (exact `https://host[:port]` origins) and a call's `url` must be on one. NAMES: an agent calls `<capability>__<tool>` (exposes.json's qualified name, server = capability); app UI calls the function `<capability>.<tool>` (it must be in the package's `functions` allowlist like any other function); BOTH parse to the same `{capability, tool}` (`parseCases`), anything else is `unknown`. THE BROKER (`decideCases`, `brokerDecide`) is ONE function for UI and agent calls: the FIRST of: `unknown` (no such capability or tool: the agent answer is MCP's unknown-tool error -32602, never a hint) · `denied/hostOnly` · `denied/notDeclared` (the manifest is the CEILING: no scope declared = no call, no sheet) · `unavailable` (this host does not provide the capability, e.g. `camera` on a desktop without one) · `denied/originNotDeclared` (an `origins` scope whose `url` argument is missing or off the list) · for `picker`: `denied/needsForeground` in the background, else `call` · for `ask`: the stored grant `granted` = `call`, `denied` = `denied/consentDenied`, undecided = `ask` in the foreground (the Engine shows the consent sheet; consent.json `consentAnswer` turns the answer into the call's verdict) and `denied/consentNeeded` in the background (never a sheet behind the user's back). An agent's tools/list (`listCases`, `agentToolList`) = every tool of a scope the manifest declares and the host provides, `hostOnly` excluded, sorted by qualified name; a denied scope stays listed (the call answers `consentDenied`, so the agent can tell the person). THE AUDIT (`auditCases`, `brokerAuditRow`): every call, refused ones included, writes ONE row in audit.json's shape to the host's sink: `appId` = the package id, `clientId` = `ui` for app UI or the run id for an agent, `tool` = the qualified name `<capability>__<tool>` for both, `status` = `denied` for every refusal and `unknown`/`unavailable`, else audit.json's status of the call's outcome (`ok`, `isError` -> failed, `invalidParams` -> invalid, `rpcError`/`unreachable`/`timeout` -> upstream), `error` = the refusal's reason (`unknownTool` for unknown, `unavailable`), null on ok and failed; `argsHash` = audit.json's hash of the arguments as received. No hub tool writes the package, the grants or the host config: a grant changes only in consent.json's `grantWrite`. CAPABILITIES V1 (VAPP-265, E5): the TOOLS (`tools`, sorted by name) lock every tool's name, its app-facing `inputSchema`/`outputSchema` (what app UI and agents see: an agent's tools/list lists them as `<capability>__<tool>` with the same description and schemas) and, for `runs: native`, the `nativeOutputSchema` a host's native provider answers; `runs: engine` = the Engine answers it itself (`print_pdf`, no native call; the capability must still be provided). A host's native capability implements EVERY native tool of its capability (one it cannot serve answers isError `unavailable`). THE BRIDGE (`bridgeCases`, `bridgeCall`) runs between the broker's `call` verdict and the native, the same for UI and agents: (1) VALIDATE the arguments (`validateCases`) against `inputSchema` with the subset `type` (one or a list; `integer` = an integral number) · `properties` · `required` · `additionalProperties: false` · `minimum`/`maximum` · `minLength`/`maxLength` (code points) · `minItems`/`maxItems` · `items` · `enum` · `format` (`date-time` = RFC 3339 with a zone; `token` = 1 to 64 of `[A-Za-z0-9._-]`; `uri` = an absolute http(s) URL; `base64` = the standard alphabet, padded) · `dependentRequired` · `anyOf`/`oneOf` of `required` sets; checked in that order per value: type, then for an object required (listed order), unknown keys (sorted), each property (schema order), dependentRequired (its key order; the path is the MISSING key's), anyOf, oneOf; for an array minItems, maxItems, then each item; the FIRST violation answers isError `invalidParams: <JSON pointer, / for the root> <keyword>` (audit outcome `invalidParams`) and the native never runs; a missing top-level property with a `default` gets it. (2) MAP to the native arguments: photos.*, files_open = the validated arguments; files_save = `{name, mimeType, data}`; share_send = the given `title`/`text`/`url` plus `file: {name, mimeType, data}` for a `fileId`; print_print = `{name, mimeType, data}` (`name` default `document.<pdf|png|jpg>`); notifications_post = `{id: \"<app>/<id>\", title, body|null, at|null, tap}` where `tap` = lifecycle.json's `{type: notificationTap, app, screen, params ({} default)}` or null without a `screen` (a `screen` must be one of the package's screens, else `invalidParams: /screen enum`) and an absent `id` is generated (`n-` + 32 lowercase hex), so a host hands the tap back as the instance's event and ids never collide across apps; notifications_cancel = `{id: \"<app>/<id>\"}`. A `fileId` resolves through the host's BlobStore (`v1.context` = the app and its screens; missing = isError `notFound: <fileId>`), `data` = its bytes in base64; `text` = its UTF-8 bytes. `mimeType` = the given one, else `v1.mimeByExtension` of the name's last `.ext` (case-insensitive), else sniffed from the bytes (`%PDF-` application/pdf, FF D8 FF image/jpeg, the PNG signature image/png, `GIF8` image/gif), else text/plain for a `text` and application/octet-stream; print_print takes only a sniffed PDF, PNG or JPEG (else isError `unsupportedFile`). (3) CHECK the native's result: an `isError` result passes through as is (audit `failed`); otherwise its `structuredContent` must match `nativeOutputSchema` (else isError `badResult: <pointer> <keyword>`, nothing stored). (4) STORE every object in it that carries `data` (depth first, in order): BlobStore `put(app, name, mimeType, bytes)` (ids in the cases: `blob-1`, `blob-2`, …), the object loses `data` and gains `size` (bytes) and `fileId`; notifications_post answers `{id}` (the app's own id). Every answer is an MCP call result `{content: [{type: text, text: <the structuredContent as JSON>}], structuredContent}`, errors `{content: [{type: text, text: <reason>}], isError: true}`. THE PDF (`pdf`, `pdfCases`, `print_pdf`): ONE layout both twins write byte for byte (sha256 + size locked): A4 (`pdf.page` points), margins 56, content width 483, the cursor y from 786 down to 56; fonts F1 Helvetica and F2 Helvetica-Bold (Type1, WinAnsiEncoding; `pdf.widths` = their widths per WinAnsi code in 1/1000 em, `pdf.winAnsi` = the code points 0x80-0x9F map to; ASCII 0x20-0x7E and 0xA0-0xFF are themselves; a tab is a space, other controls drop, anything else is `?`). A text's width at size s = sum(widths) × s / 1000, compared exactly (sum × s ≤ 483000). Lines: a paragraph per `\\n`, words split on runs of spaces, greedy, a word wider than the line broken at the longest fitting prefix (one character at least); an empty paragraph is one empty line. A line at size s and leading L: if the page has content and y − L < 56, a new page; then `BT /F<n> <s> Tf 56 <y − s> Td (<text>) Tj ET` (an empty line writes nothing) and y −= L. heading: y −= 12 first unless the page is empty, F2 18/24, then y −= 4. text: F1 11/15, then y −= 6. image (a JPEG: SOF0-SOF3 width, height and 1/3/4 components = DeviceGray/RGB/CMYK, DCTDecode as is; a PNG: 8-bit, colour type 0 or 2, not interlaced, its IDATs concatenated with FlateDecode + DecodeParms Predictor 15; anything else isError `unsupportedImage: <fileId>`, a missing file `notFound: <fileId>`): scale = min(483/w, 730/h, 1), if the page has content and y − h·scale < 56 a new page, `q <w·scale> 0 0 <h·scale> 56 <y − h·scale> cm /Im<k> Do Q`, y −= h·scale; its caption: y −= 4, F1 9/12 lines; then y −= 6. pageBreak: a new page unless the page is empty. The last page is dropped if it is empty and not the only one. Numbers print rounded to 2 decimals, shortest form (`12`, `12.5`). Text strings escape `(`, `)` and `\\` and write bytes ≥ 0x80 as 3-digit octal. The file: `%PDF-1.4\\n%` E2 E3 CF D3 `\\n`; objects `<n> 0 obj\\n<body>\\nendobj\\n`: 1 the Catalog, 2 Pages (Kids, Count), 3 F1, 4 F2, 5 Info (`/Title <FEFF…>` UTF-16BE uppercase hex, `/Producer (Exponential Engine)`), then one XObject per image file in first-use order (`/Im1`…, a file used twice is one object), then per page its Page (MediaBox, `/Resources << /Font << /F1 3 0 R /F2 4 0 R >> /XObject << … >> >>`, the XObject dict only with the page's images in number order) and its content; a stream body is `<< … /Length <n> >>\\nstream\\n<data>\\nendstream` (content data = the page's operators joined by `\\n`); then `xref\\n0 <size>\\n0000000000 65535 f \\n`, one `<10-digit offset> 00000 n \\n` per object, `trailer\\n<< /Size <size> /Root 1 0 R /Info 5 0 R >>\\nstartxref\\n<offset>\\n%%EOF\\n`. The file is stored as `name` (default the title with `/\\:*?\"<>|` and controls replaced by `-`, + `.pdf`), application/pdf; the answer `{file, pages}`. EDE (#VAPP-273): Exponential Desktop's own capabilities over ede-protocol, `windows` (`windows.read`: `list`), `focus` (`focus.window`: `window`) and `launch` (`launch.app`: `app`), each `ask`, provided by eDE's shell only (`host: ede` marks them: every other host answers `unavailable`, and a platform's native suite replays only the tools of capabilities without a `host`); the host-only `shell.edit` has no v1 tool here: eDE's shell serves `shell__edit` to its own agent from its own provider, and the broker refuses it for every app. Their bridge is the plain one: the validated arguments are the native's, the native's result is the answer.",
|
|
3
3
|
"scopePattern": "^[a-z][a-z0-9]*\\.[a-z][a-z0-9-]*$",
|
|
4
4
|
"reservedNames": ["app", "bind", "consent", "engine", "exponential", "records", "upstream"],
|
|
5
5
|
"capabilities": [
|
|
@@ -24,11 +24,23 @@
|
|
|
24
24
|
{ "id": "files.write", "verb": "save files where you pick", "consent": "picker", "tools": ["save"] }
|
|
25
25
|
]
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
"name": "focus",
|
|
29
|
+
"label": "Focus",
|
|
30
|
+
"host": "ede",
|
|
31
|
+
"scopes": [{ "id": "focus.window", "verb": "switch to your windows", "consent": "ask", "tools": ["window"] }]
|
|
32
|
+
},
|
|
27
33
|
{
|
|
28
34
|
"name": "http",
|
|
29
35
|
"label": "Network",
|
|
30
36
|
"scopes": [{ "id": "http.fetch", "verb": "connect to {origins}", "consent": "ask", "origins": true, "tools": ["fetch"] }]
|
|
31
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"name": "launch",
|
|
40
|
+
"label": "Launch",
|
|
41
|
+
"host": "ede",
|
|
42
|
+
"scopes": [{ "id": "launch.app", "verb": "start applications", "consent": "ask", "tools": ["app"] }]
|
|
43
|
+
},
|
|
32
44
|
{
|
|
33
45
|
"name": "location",
|
|
34
46
|
"label": "Location",
|
|
@@ -58,6 +70,12 @@
|
|
|
58
70
|
"name": "shell",
|
|
59
71
|
"label": "Desktop",
|
|
60
72
|
"scopes": [{ "id": "shell.edit", "verb": "change your desktop", "consent": "ask", "hostOnly": true, "tools": ["edit"] }]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "windows",
|
|
76
|
+
"label": "Windows",
|
|
77
|
+
"host": "ede",
|
|
78
|
+
"scopes": [{ "id": "windows.read", "verb": "see your open windows", "consent": "ask", "tools": ["list"] }]
|
|
61
79
|
}
|
|
62
80
|
],
|
|
63
81
|
"parseCases": [
|
|
@@ -215,6 +233,33 @@
|
|
|
215
233
|
"foreground": true,
|
|
216
234
|
"call": { "from": "ui", "name": "http_fetch", "arguments": {} },
|
|
217
235
|
"expected": { "verdict": "denied", "capability": "http", "tool": "fetch", "scope": "http.fetch", "reason": "originNotDeclared" }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "eDE: a granted windows scope lists for an agent in the background",
|
|
239
|
+
"manifest": [{ "id": "windows.read", "why": "Show the open windows." }],
|
|
240
|
+
"grants": { "windows.read": "granted" },
|
|
241
|
+
"provided": ["windows", "focus", "launch"],
|
|
242
|
+
"foreground": false,
|
|
243
|
+
"call": { "from": "agent", "name": "windows__list", "arguments": {} },
|
|
244
|
+
"expected": { "verdict": "call", "capability": "windows", "tool": "list", "scope": "windows.read", "reason": null }
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"name": "eDE: launch is denied until the manifest declares it",
|
|
248
|
+
"manifest": [{ "id": "windows.read", "why": "Show the open windows." }],
|
|
249
|
+
"grants": {},
|
|
250
|
+
"provided": ["windows", "focus", "launch"],
|
|
251
|
+
"foreground": true,
|
|
252
|
+
"call": { "from": "ui", "name": "launch_app", "arguments": {"app": "org.gnome.Calculator"} },
|
|
253
|
+
"expected": { "verdict": "denied", "capability": "launch", "tool": "app", "scope": "launch.app", "reason": "notDeclared" }
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "eDE: focus asks once in the foreground like any ask scope",
|
|
257
|
+
"manifest": [{ "id": "focus.window", "why": "Switch to a window from the list." }],
|
|
258
|
+
"grants": {},
|
|
259
|
+
"provided": ["windows", "focus", "launch"],
|
|
260
|
+
"foreground": true,
|
|
261
|
+
"call": { "from": "ui", "name": "focus_window", "arguments": {"window": "w3"} },
|
|
262
|
+
"expected": { "verdict": "ask", "capability": "focus", "tool": "window", "scope": "focus.window", "reason": null }
|
|
218
263
|
}
|
|
219
264
|
],
|
|
220
265
|
"listCases": [
|
|
@@ -234,6 +279,12 @@
|
|
|
234
279
|
"manifest": [],
|
|
235
280
|
"provided": ["photos", "share"],
|
|
236
281
|
"expected": []
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "eDE's scopes list; shell.edit stays host-only",
|
|
285
|
+
"manifest": [{ "id": "windows.read", "why": "Show the open windows." }, { "id": "focus.window", "why": "Switch to a window from the list." }, { "id": "launch.app", "why": "Start the calculator from the tile." }, { "id": "shell.edit", "why": "Rearrange the panel." }],
|
|
286
|
+
"provided": ["windows", "focus", "launch", "shell"],
|
|
287
|
+
"expected": ["focus__window", "launch__app", "windows__list"]
|
|
237
288
|
}
|
|
238
289
|
],
|
|
239
290
|
"auditCases": [
|
|
@@ -296,6 +347,8 @@
|
|
|
296
347
|
"tools": [
|
|
297
348
|
{"name": "files_open", "runs": "native", "description": "Let the person pick files to open. `accept` = MIME types (`image/*`) or extensions (`.csv`). Answers the files, each stored as an app file; an empty list when they cancel.", "inputSchema": {"type": "object", "properties": {"accept": {"type": "array", "maxItems": 20, "items": {"type": "string", "minLength": 1, "maxLength": 100}}, "multiple": {"type": "boolean", "default": false}}, "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"files": {"type": "array", "maxItems": 100, "items": {"type": "object", "properties": {"name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "data": {"type": "string", "format": "base64"}}, "required": ["name", "mimeType", "data"], "additionalProperties": false}}}, "required": ["files"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"files": {"type": "array", "maxItems": 100, "items": {"type": "object", "properties": {"name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "size": {"type": "integer", "minimum": 0}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["name", "mimeType", "size", "fileId"], "additionalProperties": false}}}, "required": ["files"], "additionalProperties": false}},
|
|
298
349
|
{"name": "files_save", "runs": "native", "description": "Let the person save an app file (`fileId`) or a text (`text`) where they pick. `saved` = false when they cancel.", "inputSchema": {"type": "object", "properties": {"name": {"type": "string", "minLength": 1, "maxLength": 255}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}, "text": {"type": "string", "maxLength": 1000000}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}}, "required": ["name"], "additionalProperties": false, "oneOf": [{"required": ["fileId"]},{"required": ["text"]}]}, "nativeOutputSchema": {"type": "object", "properties": {"saved": {"type": "boolean"}}, "required": ["saved"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"saved": {"type": "boolean"}}, "required": ["saved"], "additionalProperties": false}},
|
|
350
|
+
{"name": "focus_window", "runs": "native", "description": "Bring a window of this desktop to the front (its id from windows_list); a minimized one comes back first, a parked one brings its workspace along. `focused` = false when the window is gone.", "inputSchema": {"type": "object", "properties": {"window": {"type": "string", "format": "token"}}, "required": ["window"], "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"focused": {"type": "boolean"}}, "required": ["focused"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"focused": {"type": "boolean"}}, "required": ["focused"], "additionalProperties": false}},
|
|
351
|
+
{"name": "launch_app", "runs": "native", "description": "Start an installed application by its id (a desktop entry's, `org.gnome.Calculator`, with or without `.desktop`). `launched` = false when nothing is installed under that id.", "inputSchema": {"type": "object", "properties": {"app": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["app"], "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"launched": {"type": "boolean"}}, "required": ["launched"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"launched": {"type": "boolean"}}, "required": ["launched"], "additionalProperties": false}},
|
|
299
352
|
{"name": "notifications_cancel", "runs": "native", "description": "Withdraw a notification this app posted or scheduled.", "inputSchema": {"type": "object", "properties": {"id": {"type": "string", "format": "token"}}, "required": ["id"], "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"cancelled": {"type": "boolean"}}, "required": ["cancelled"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"cancelled": {"type": "boolean"}}, "required": ["cancelled"], "additionalProperties": false}},
|
|
300
353
|
{"name": "notifications_post", "runs": "native", "description": "Post a notification now, or at `at` (an ISO time). A tap opens `screen` with `params`. Posting an `id` again replaces it.", "inputSchema": {"type": "object", "properties": {"id": {"type": "string", "format": "token"}, "title": {"type": "string", "minLength": 1, "maxLength": 200}, "body": {"type": "string", "minLength": 0, "maxLength": 1000}, "at": {"type": "string", "format": "date-time"}, "screen": {"type": "string", "minLength": 1, "maxLength": 100}, "params": {"type": "object"}}, "required": ["title"], "additionalProperties": false, "dependentRequired": {"params": ["screen"]}}, "nativeOutputSchema": {"type": "object", "properties": {}, "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"id": {"type": "string", "format": "token"}}, "required": ["id"], "additionalProperties": false}},
|
|
301
354
|
{"name": "photos_get", "runs": "native", "description": "Read one photo of the library by its id (from list or pick), stored as an app file.", "inputSchema": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 500}}, "required": ["id"], "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"photo": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 500}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "takenAt": {"type": ["string", "null"], "format": "date-time"}, "data": {"type": "string", "format": "base64"}}, "required": ["id", "name", "mimeType", "takenAt", "data"], "additionalProperties": false}}, "required": ["photo"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"photo": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 500}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "takenAt": {"type": ["string", "null"], "format": "date-time"}, "size": {"type": "integer", "minimum": 0}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["id", "name", "mimeType", "takenAt", "size", "fileId"], "additionalProperties": false}}, "required": ["photo"], "additionalProperties": false}},
|
|
@@ -303,7 +356,8 @@
|
|
|
303
356
|
{"name": "photos_pick", "runs": "native", "description": "Let the person pick photos in the platform's photo picker. Answers the picked photos, each stored as an app file; an empty list when they cancel.", "inputSchema": {"type": "object", "properties": {"limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 1}}, "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"photos": {"type": "array", "maxItems": 50, "items": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 500}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "takenAt": {"type": ["string", "null"], "format": "date-time"}, "data": {"type": "string", "format": "base64"}}, "required": ["id", "name", "mimeType", "takenAt", "data"], "additionalProperties": false}}}, "required": ["photos"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"photos": {"type": "array", "maxItems": 50, "items": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 500}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "takenAt": {"type": ["string", "null"], "format": "date-time"}, "size": {"type": "integer", "minimum": 0}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["id", "name", "mimeType", "takenAt", "size", "fileId"], "additionalProperties": false}}}, "required": ["photos"], "additionalProperties": false}},
|
|
304
357
|
{"name": "print_pdf", "runs": "engine", "description": "Lay out a document (headings, text, app images, page breaks) as an A4 PDF, stored as an app file. No dialog: save, share or print the file next.", "inputSchema": {"type": "object", "properties": {"title": {"type": "string", "minLength": 1, "maxLength": 200}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "blocks": {"type": "array", "minItems": 1, "maxItems": 2000, "items": {"type": "object", "properties": {"heading": {"type": "string", "minLength": 1, "maxLength": 500}, "text": {"type": "string", "minLength": 0, "maxLength": 20000}, "image": {"type": "string", "minLength": 1, "maxLength": 200}, "caption": {"type": "string", "minLength": 1, "maxLength": 500}, "pageBreak": {"type": "boolean", "enum": [true]}}, "additionalProperties": false, "oneOf": [{"required": ["heading"]},{"required": ["text"]},{"required": ["image"]},{"required": ["pageBreak"]}], "dependentRequired": {"caption": ["image"]}}}}, "required": ["title", "blocks"], "additionalProperties": false}, "nativeOutputSchema": null, "outputSchema": {"type": "object", "properties": {"file": {"type": "object", "properties": {"name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}, "size": {"type": "integer", "minimum": 0}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}}, "required": ["name", "mimeType", "size", "fileId"], "additionalProperties": false}, "pages": {"type": "integer", "minimum": 1}}, "required": ["file", "pages"], "additionalProperties": false}},
|
|
305
358
|
{"name": "print_print", "runs": "native", "description": "Open the platform's print dialog for an app file (a PDF, PNG or JPEG). `printed` = false when they cancel.", "inputSchema": {"type": "object", "properties": {"fileId": {"type": "string", "minLength": 1, "maxLength": 200}, "name": {"type": "string", "minLength": 1, "maxLength": 255}}, "required": ["fileId"], "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"printed": {"type": "boolean"}}, "required": ["printed"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"printed": {"type": "boolean"}}, "required": ["printed"], "additionalProperties": false}},
|
|
306
|
-
{"name": "share_send", "runs": "native", "description": "Open the platform's share sheet with a text, a link and/or an app file. `shared` = false when they dismiss it (true where the platform cannot tell).", "inputSchema": {"type": "object", "properties": {"title": {"type": "string", "minLength": 1, "maxLength": 200}, "text": {"type": "string", "minLength": 1, "maxLength": 10000}, "url": {"type": "string", "format": "uri"}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}}, "additionalProperties": false, "anyOf": [{"required": ["text"]},{"required": ["url"]},{"required": ["fileId"]}], "dependentRequired": {"fileId": ["name"], "mimeType": ["fileId"]}}, "nativeOutputSchema": {"type": "object", "properties": {"shared": {"type": "boolean"}}, "required": ["shared"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"shared": {"type": "boolean"}}, "required": ["shared"], "additionalProperties": false}}
|
|
359
|
+
{"name": "share_send", "runs": "native", "description": "Open the platform's share sheet with a text, a link and/or an app file. `shared` = false when they dismiss it (true where the platform cannot tell).", "inputSchema": {"type": "object", "properties": {"title": {"type": "string", "minLength": 1, "maxLength": 200}, "text": {"type": "string", "minLength": 1, "maxLength": 10000}, "url": {"type": "string", "format": "uri"}, "fileId": {"type": "string", "minLength": 1, "maxLength": 200}, "name": {"type": "string", "minLength": 1, "maxLength": 255}, "mimeType": {"type": "string", "minLength": 1, "maxLength": 100}}, "additionalProperties": false, "anyOf": [{"required": ["text"]},{"required": ["url"]},{"required": ["fileId"]}], "dependentRequired": {"fileId": ["name"], "mimeType": ["fileId"]}}, "nativeOutputSchema": {"type": "object", "properties": {"shared": {"type": "boolean"}}, "required": ["shared"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"shared": {"type": "boolean"}}, "required": ["shared"], "additionalProperties": false}},
|
|
360
|
+
{"name": "windows_list", "runs": "native", "description": "The open windows of this desktop, protected ones left out: each window's id, title, app (its desktop entry id), workspace, whether it is focused and whether it is minimized.", "inputSchema": {"type": "object", "properties": {}, "additionalProperties": false}, "nativeOutputSchema": {"type": "object", "properties": {"windows": {"type": "array", "maxItems": 500, "items": {"type": "object", "properties": {"id": {"type": "string", "format": "token"}, "title": {"type": "string", "maxLength": 500}, "app": {"type": "string", "maxLength": 200}, "workspace": {"type": "integer", "minimum": 0}, "focused": {"type": "boolean"}, "minimized": {"type": "boolean"}}, "required": ["id", "title", "app", "workspace", "focused", "minimized"], "additionalProperties": false}}}, "required": ["windows"], "additionalProperties": false}, "outputSchema": {"type": "object", "properties": {"windows": {"type": "array", "maxItems": 500, "items": {"type": "object", "properties": {"id": {"type": "string", "format": "token"}, "title": {"type": "string", "maxLength": 500}, "app": {"type": "string", "maxLength": 200}, "workspace": {"type": "integer", "minimum": 0}, "focused": {"type": "boolean"}, "minimized": {"type": "boolean"}}, "required": ["id", "title", "app", "workspace", "focused", "minimized"], "additionalProperties": false}}}, "required": ["windows"], "additionalProperties": false}}
|
|
307
361
|
],
|
|
308
362
|
"validateCases": [
|
|
309
363
|
{"tool": "photos_pick", "arguments": {}, "expected": {"arguments": {"limit": 1}}},
|
|
@@ -331,7 +385,11 @@
|
|
|
331
385
|
{"tool": "print_pdf", "arguments": {"title": "A", "blocks": [{"heading": "x", "text": "y"}]}, "expected": {"error": "invalidParams: /blocks/0 oneOf"}},
|
|
332
386
|
{"tool": "print_pdf", "arguments": {"title": "A", "blocks": [{"text": "y", "caption": "c"}]}, "expected": {"error": "invalidParams: /blocks/0/image dependentRequired"}},
|
|
333
387
|
{"tool": "print_pdf", "arguments": {"title": "A", "blocks": [{"pageBreak": false}]}, "expected": {"error": "invalidParams: /blocks/0/pageBreak enum"}},
|
|
334
|
-
{"tool": "print_print", "arguments": {"fileId": 7}, "expected": {"error": "invalidParams: /fileId type"}}
|
|
388
|
+
{"tool": "print_print", "arguments": {"fileId": 7}, "expected": {"error": "invalidParams: /fileId type"}},
|
|
389
|
+
{"tool": "focus_window", "arguments": {}, "expected": {"error": "invalidParams: /window required"}},
|
|
390
|
+
{"tool": "focus_window", "arguments": {"window": "w3"}, "expected": {"arguments": {"window": "w3"}}},
|
|
391
|
+
{"tool": "launch_app", "arguments": {"app": ""}, "expected": {"error": "invalidParams: /app minLength"}},
|
|
392
|
+
{"tool": "windows_list", "arguments": {"since": 1}, "expected": {"error": "invalidParams: /since additionalProperties"}}
|
|
335
393
|
],
|
|
336
394
|
"bridgeCases": [
|
|
337
395
|
{"name": "a picked photo lands in the app's files", "tool": "photos_pick", "arguments": {"limit": 2}, "native": {"limit": 2}, "nativeResult": {"structuredContent": {"photos": [{"id": "ph-1", "name": "IMG_0001.jpg", "mimeType": "image/jpeg", "takenAt": "2026-11-01T10:00:00Z", "data": "aGk="}]}}, "puts": [{"name": "IMG_0001.jpg", "mimeType": "image/jpeg", "base64": "aGk="}], "expected": {"structuredContent": {"photos": [{"id": "ph-1", "name": "IMG_0001.jpg", "mimeType": "image/jpeg", "takenAt": "2026-11-01T10:00:00Z", "size": 2, "fileId": "blob-1"}]}}},
|
|
@@ -357,7 +415,10 @@
|
|
|
357
415
|
{"name": "invalid arguments never reach the native", "tool": "photos_pick", "arguments": {"limit": 0}, "native": null, "nativeResult": null, "puts": [], "expected": {"isError": "invalidParams: /limit minimum", "outcome": "invalidParams"}},
|
|
358
416
|
{"name": "a native result off the schema is refused, nothing stored", "tool": "photos_pick", "arguments": {}, "native": {"limit": 1}, "nativeResult": {"structuredContent": {"photos": [{"id": "x"}]}}, "puts": [], "expected": {"isError": "badResult: /photos/0/name required", "outcome": "isError"}},
|
|
359
417
|
{"name": "bytes that are not base64 are refused", "tool": "photos_get", "arguments": {"id": "ph-1"}, "native": {"id": "ph-1"}, "nativeResult": {"structuredContent": {"photo": {"id": "ph-1", "name": "IMG_0001.jpg", "mimeType": "image/jpeg", "takenAt": "2026-11-01T10:00:00Z", "data": "a b"}}}, "puts": [], "expected": {"isError": "badResult: /photo/data format", "outcome": "isError"}},
|
|
360
|
-
{"name": "a native result without structuredContent is refused", "tool": "share_send", "arguments": {"text": "x"}, "native": {"text": "x"}, "nativeResult": {"content": [{"type": "text", "text": "ok"}]}, "puts": [], "expected": {"isError": "badResult: / type", "outcome": "isError"}}
|
|
418
|
+
{"name": "a native result without structuredContent is refused", "tool": "share_send", "arguments": {"text": "x"}, "native": {"text": "x"}, "nativeResult": {"content": [{"type": "text", "text": "ok"}]}, "puts": [], "expected": {"isError": "badResult: / type", "outcome": "isError"}},
|
|
419
|
+
{"name": "eDE: the window list passes through as the native answers it", "tool": "windows_list", "arguments": {}, "native": {}, "nativeResult": {"structuredContent": {"windows": [{"id": "w1", "title": "Firefox", "app": "firefox", "workspace": 0, "focused": true, "minimized": false}, {"id": "w2", "title": "", "app": "org.gnome.Calculator", "workspace": 1, "focused": false, "minimized": true}]}}, "puts": [], "expected": {"structuredContent": {"windows": [{"id": "w1", "title": "Firefox", "app": "firefox", "workspace": 0, "focused": true, "minimized": false}, {"id": "w2", "title": "", "app": "org.gnome.Calculator", "workspace": 1, "focused": false, "minimized": true}]}}},
|
|
420
|
+
{"name": "eDE: focus names the window as given", "tool": "focus_window", "arguments": {"window": "w2"}, "native": {"window": "w2"}, "nativeResult": {"structuredContent": {"focused": true}}, "puts": [], "expected": {"structuredContent": {"focused": true}}},
|
|
421
|
+
{"name": "eDE: a launch of an unknown id is not an error, just not launched", "tool": "launch_app", "arguments": {"app": "nope"}, "native": {"app": "nope"}, "nativeResult": {"structuredContent": {"launched": false}}, "puts": [], "expected": {"structuredContent": {"launched": false}}}
|
|
361
422
|
],
|
|
362
423
|
"pdf": {"page": [595,842], "margin": 56, "fonts": {"F1": "Helvetica", "F2": "Helvetica-Bold"}, "widths": {"Helvetica": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,278,278,355,556,556,889,667,191,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,278,278,584,584,584,556,1015,667,667,722,722,667,611,778,722,278,500,667,556,833,722,778,667,778,722,667,611,722,667,944,667,667,611,278,278,278,469,556,333,556,556,500,556,556,278,556,556,222,222,500,222,833,556,556,556,556,333,500,278,556,500,722,500,500,500,334,260,334,584,0,556,0,222,556,333,1000,556,556,333,1000,667,333,1000,0,611,0,0,222,222,333,333,350,556,1000,333,1000,500,333,944,0,500,667,278,333,556,556,556,556,260,556,333,737,370,556,584,333,737,333,400,584,333,333,333,556,537,278,333,333,365,556,834,834,834,611,667,667,667,667,667,667,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,500,556,556,556,556,278,278,278,278,556,556,556,556,556,556,556,584,611,556,556,556,556,500,556,500], "Helvetica-Bold": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,278,333,474,556,556,889,722,238,333,333,389,584,278,333,278,278,556,556,556,556,556,556,556,556,556,556,333,333,584,584,584,611,975,722,722,722,722,667,611,778,722,278,556,722,611,833,722,778,667,778,722,667,611,722,667,944,667,667,611,333,278,333,584,556,333,556,611,556,611,556,333,611,611,278,278,556,278,889,611,611,611,611,389,556,333,611,556,778,556,556,500,389,280,389,584,0,556,0,278,556,500,1000,556,556,333,1000,667,333,1000,0,611,0,0,278,278,500,500,350,556,1000,333,1000,556,333,944,0,500,667,278,333,556,556,556,556,280,556,333,737,370,556,584,333,737,333,400,584,333,333,333,611,556,278,333,333,365,556,834,834,834,611,722,722,722,722,722,722,1000,722,667,667,667,667,278,278,278,278,722,722,778,778,778,778,778,584,778,722,722,722,722,667,667,611,556,556,556,556,556,556,889,556,556,556,556,556,278,278,278,278,611,611,611,611,611,611,611,584,611,611,611,611,611,556,611,556]}, "winAnsi": {"338": 140, "339": 156, "352": 138, "353": 154, "376": 159, "381": 142, "382": 158, "402": 131, "710": 136, "732": 152, "8211": 150, "8212": 151, "8216": 145, "8217": 146, "8218": 130, "8220": 147, "8221": 148, "8222": 132, "8224": 134, "8225": 135, "8226": 149, "8230": 133, "8240": 137, "8249": 139, "8250": 155, "8364": 128, "8482": 153}},
|
|
363
424
|
"pdfCases": [
|
package/fixtures/outbox.json
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
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.",
|
|
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
3
|
"$ref": "../../domain-contract/fixtures/app-records.json#/outbox",
|
|
4
4
|
"modeCases": [
|
|
5
5
|
{ "name": "a sync provider queues", "sync": true, "expected": "queue" },
|
|
6
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"}]}
|
|
7
21
|
]
|
|
8
22
|
}
|
package/fixtures/providers.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"hosts": {
|
|
22
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
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" }] },
|
|
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
25
|
"spezl": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "peer" }, "vault": {}, "window": {}, "network": "any", "sync": {}, "agents": [{ "id": "acp" }] },
|
|
26
26
|
"exported": { "host": {}, "storage": {}, "blobs": {}, "identity": { "kind": "local" }, "vault": {}, "window": {}, "network": "off", "capabilities": [{ "name": "photos" }, { "name": "print" }, { "name": "share" }] }
|
|
27
27
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exponential_app/engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The Engine's contract: an app instance's lifecycle, consent and grants, the one capability broker for app UI and agents, the host providers and the outbox mode (fixtures + the pure TypeScript reference, the web's twin; the eengine crate replays them)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://exponential.at/sdk/apps/",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|