@zackbart/connecta 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/apps-shell.d.ts +10 -12
- package/dist/apps-shell.js +29 -220
- package/dist/execute.d.ts +0 -7
- package/dist/execute.js +8 -117
- package/dist/index.js +108 -67
- package/dist/invocation.d.ts +1 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +8 -493
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/code-mode.md +19 -19
- package/documentation/meta-tools.md +4 -30
- package/documentation/operations.md +13 -13
- package/documentation/provider-conventions.md +3 -4
- package/documentation/upgrading.md +34 -8
- package/ethos.md +1 -1
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.20.0 — 2026-08-26
|
|
6
|
+
|
|
7
|
+
This release removes the two side languages that had grown around the seven
|
|
8
|
+
tools: direct-call field projection and live reads from program-rendered HTML.
|
|
9
|
+
Agents now shape data in JavaScript, page a genuinely large direct result with
|
|
10
|
+
`get_result`, and render a display-only view. Configuration becomes strict at
|
|
11
|
+
the same boundary, so a typo fails at startup instead of surviving as inert
|
|
12
|
+
deployment state. Existing deployments using current documented options need
|
|
13
|
+
only bump the pin and reconcile generation B; callers that send `fields`,
|
|
14
|
+
programs that bind UI reads, and JavaScript deployments carrying retired or
|
|
15
|
+
misspelled options must migrate. The implementation stack deletes 2,866 lines
|
|
16
|
+
and adds 1,063 before these release notes, while preserving exactly seven MCP
|
|
17
|
+
tools and the Node and Worker deployment shapes.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **One data-shaping language.** `call_tool` and `call_destructive_tool` no
|
|
22
|
+
longer advertise or accept `fields`; the path resolver, schema walker,
|
|
23
|
+
projection recovery records, guidance, and tests are gone. `execute_code`
|
|
24
|
+
remains the projection surface, while `get_result` still pages oversized
|
|
25
|
+
direct reads byte-exactly. The 21-scenario current-version audit passes, fixed
|
|
26
|
+
tool definitions fall from 1,625 to 1,587 tokens, and a deterministic
|
|
27
|
+
52,396-byte document succeeds 3/3 through `call_tool` then `get_result`
|
|
28
|
+
(#482).
|
|
29
|
+
- **Display-only program UI.** `connecta.ui(html)` keeps success-only Apps
|
|
30
|
+
delivery, sandboxing, one shared rich-output budget, executor parity, and the
|
|
31
|
+
compact mirrored return. Its read manifest, host-call bridge, and second
|
|
32
|
+
argument are removed. All seven tools explicitly remain model-only, and only
|
|
33
|
+
`execute_code` advertises the cache-busted v3 display resource, so neither a
|
|
34
|
+
current nor cached shell can call Connecta tools (#484).
|
|
35
|
+
- **Strict configuration.** `createConnecta` rejects unknown own options at
|
|
36
|
+
every closed configuration path without reading or quoting their values.
|
|
37
|
+
Each closed schema is compile-time exhaustive against its public type;
|
|
38
|
+
connector, auth, storage, activity-store, logger, deployment-metadata, and
|
|
39
|
+
executor implementations remain open leaves (#485).
|
|
40
|
+
- **Smaller package and tests.** Generated operator assets publish explicit
|
|
41
|
+
95-byte string declarations instead of embedding about 95 KB of literals,
|
|
42
|
+
with byte-identical runtime output and a packed-package size guard (#486).
|
|
43
|
+
The remaining copied UI credential and meta-tool fixtures are shared without
|
|
44
|
+
changing any test or assertion count, removing another 127 repository lines
|
|
45
|
+
(#479).
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
- The direct-call `fields` projection option and its dot/array path language.
|
|
50
|
+
- Program UI read bindings, `connecta.read`, and the Apps-to-host tool-call
|
|
51
|
+
bridge.
|
|
52
|
+
|
|
53
|
+
### Declined with evidence
|
|
54
|
+
|
|
55
|
+
- Discovery filtering did not ship: its sealed qualification retained complete
|
|
56
|
+
scenario coverage but missed top-1, recall, default-page recall, and negative
|
|
57
|
+
false-positive gates. The holdout cases were not inspected or tuned (#481).
|
|
58
|
+
- Lean object-result text did not ship: Codex and Claude read ordinary
|
|
59
|
+
structured values, but Claude's error route took extra recovery actions and
|
|
60
|
+
Cursor was unavailable for the required matrix. The compatibility JSON copy
|
|
61
|
+
remains (#483).
|
|
62
|
+
|
|
5
63
|
## 0.19.0 — 2026-08-25
|
|
6
64
|
|
|
7
65
|
This release is a smaller, simpler package with no behavioral change for a
|
package/dist/apps-shell.d.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A build-time string constant, not a file read at startup: the core is
|
|
5
5
|
* Web-API-only so it runs unchanged on Workers, and the same bytes have to
|
|
6
|
-
* serve everywhere.
|
|
7
|
-
* inside a nested `srcdoc` frame
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* serve everywhere. The shell is display-only: it renders whatever HTML a
|
|
7
|
+
* program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
|
|
8
|
+
* channel back from that frame to the host, so program-authored markup is
|
|
9
|
+
* inert beyond its own pixels.
|
|
10
10
|
*
|
|
11
11
|
* The address carries a version segment because hosts are permitted to
|
|
12
12
|
* prefetch and cache templates by URI: change these bytes, bump the version.
|
|
13
13
|
*/
|
|
14
14
|
/** The only `ui://` URI in the system. No program input reaches it. */
|
|
15
|
-
export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/
|
|
15
|
+
export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v3";
|
|
16
16
|
/** The mimeType the Apps spec requires of an HTML template. */
|
|
17
17
|
export declare const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
18
18
|
/**
|
|
@@ -29,11 +29,9 @@ export declare const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
|
|
|
29
29
|
* Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
|
|
30
30
|
* `ui/notifications/tool-result`, `ui/notifications/size-changed`,
|
|
31
31
|
* `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
|
|
32
|
-
* delivered tool result, and puts it in a frame.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
|
|
37
|
-
* no direct network.
|
|
32
|
+
* delivered tool result, and puts it in a frame. It declares no CSP domains,
|
|
33
|
+
* so the host applies its restrictive default and the `srcdoc` frame inherits
|
|
34
|
+
* `default-src 'none'`. The payload gets scripts and local interactivity, and
|
|
35
|
+
* no network.
|
|
38
36
|
*/
|
|
39
|
-
export declare const PROGRAM_UI_SHELL_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>connecta program view</title>\n <style>\n html,\n body {\n margin: 0;\n padding: 0;\n background: transparent;\n }\n #program-view {\n display: block;\n width: 100%;\n min-height: 480px;\n border: 0;\n }\n </style>\n </head>\n <body>\n <iframe\n id=\"program-view\"\n title=\"Program-rendered view\"\n sandbox=\"allow-scripts\"\n srcdoc=\"\"\n ></iframe>\n <script>\n (function () {\n \"use strict\";\n // The outer shell is the only host peer. The payload frame is\n // sandboxed to scripts alone, with no same-origin escape. Its one\n // optional message dialect is handled below and translated into\n // bounded call_tool requests; raw JSON-RPC is never forwarded.\n var host = window.parent;\n var view = document.getElementById(\"program-view\");\n var initializeId = \"connecta-ui-initialize\";\n var lastWidth = 0;\n var lastHeight = 0;\n var reads = null;\n var hostCanCallTools = false;\n var nextHostRequestId = 0;\n var pendingHostReads = Object.create(null);\n var activeHostReads = 0;\n var maxActiveHostReads = 8;\n\n function send(message) {\n if (!host || host === window) return;\n host.postMessage(message, \"*\");\n }\n\n function notify(method, params) {\n send({ jsonrpc: \"2.0\", method: method, params: params });\n }\n\n // Program views are fixed-height by construction. The shell has no\n // content-height bridge to the payload frame, so it can never learn\n // the payload's content height, and\n // what it reports here is its own box: the min-height above, unless\n // the host has given it more. Taller content scrolls inside the inner\n // frame rather than growing the view. Raising the min-height is the\n // only lever; a content-height signal would cost the isolation.\n function reportSize() {\n var width = Math.ceil(document.documentElement.clientWidth);\n var height = Math.ceil(document.documentElement.scrollHeight);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width;\n lastHeight = height;\n notify(\"ui/notifications/size-changed\", {\n width: width,\n height: height\n });\n }\n\n function payload(result) {\n if (!result || typeof result !== \"object\") return null;\n var meta = result._meta;\n if (!meta || typeof meta !== \"object\") return null;\n var value = meta[\"connecta/ui\"];\n if (!value || typeof value !== \"object\") return null;\n return typeof value.html === \"string\" && value.html.length > 0\n ? value\n : null;\n }\n\n // This function is serialized into the opaque-origin payload frame.\n // It knows no addresses and has no host channel of its own: one named\n // read request goes to the trusted outer shell and one correlated\n // result comes back.\n function payloadReadBridge() {\n \"use strict\";\n var pending = Object.create(null);\n var nextId = 0;\n\n function read(name, args) {\n return new Promise(function (resolve, reject) {\n var id = String(++nextId);\n pending[id] = { resolve: resolve, reject: reject };\n try {\n window.parent.postMessage({\n type: \"connecta/read\",\n id: id,\n name: name,\n args: args === undefined ? {} : args\n }, \"*\");\n } catch (error) {\n delete pending[id];\n reject(error);\n }\n });\n }\n\n Object.defineProperty(globalThis, \"connecta\", {\n value: Object.freeze({ read: read }),\n configurable: false,\n enumerable: true,\n writable: false\n });\n\n window.addEventListener(\"message\", function (event) {\n if (event.source !== window.parent) return;\n var message = event.data;\n if (!message || message.type !== \"connecta/read-result\") return;\n var waiter = pending[message.id];\n if (!waiter) return;\n delete pending[message.id];\n if (message.ok) waiter.resolve(message.value);\n else waiter.reject(new Error(message.error || \"Read failed\"));\n });\n }\n\n function htmlWithReadBridge(html) {\n var script =\n \"<scr\" + \"ipt>(\" + payloadReadBridge.toString() + \")();</scr\" + \"ipt>\";\n var head = /<head(?:\\s[^>]*)?>/i.exec(html);\n if (head) {\n var at = (head.index || 0) + head[0].length;\n return html.slice(0, at) + script + html.slice(at);\n }\n var document = /<html(?:\\s[^>]*)?>/i.exec(html);\n if (document) {\n var afterHtml = (document.index || 0) + document[0].length;\n return html.slice(0, afterHtml) + \"<head>\" + script + \"</head>\" + html.slice(afterHtml);\n }\n return script + html;\n }\n\n function render(params) {\n var value =\n payload(params) ||\n payload(params && params.result) ||\n payload(params && params.toolResult);\n if (value === null) return;\n reads = value.reads && typeof value.reads === \"object\"\n ? value.reads\n : null;\n view.srcdoc = reads\n ? htmlWithReadBridge(value.html)\n : value.html;\n reportSize();\n }\n\n function readError(message, fallback) {\n if (message && typeof message.message === \"string\") return message.message;\n if (message && message.data && typeof message.data.message === \"string\") {\n return message.data.message;\n }\n return fallback;\n }\n\n function finishInnerRead(innerId, ok, value) {\n if (!view.contentWindow) return;\n view.contentWindow.postMessage(ok\n ? { type: \"connecta/read-result\", id: innerId, ok: true, value: value }\n : { type: \"connecta/read-result\", id: innerId, ok: false, error: value }, \"*\");\n }\n\n function beginInnerRead(message) {\n if (!hostCanCallTools) {\n finishInnerRead(message && message.id, false, \"This host does not support app-initiated server tool calls\");\n return;\n }\n if (!message || typeof message.id !== \"string\" || typeof message.name !== \"string\") return;\n if (!reads || !Object.prototype.hasOwnProperty.call(reads, message.name)) {\n finishInnerRead(message.id, false, \"Unknown read binding\");\n return;\n }\n if (activeHostReads >= maxActiveHostReads) {\n finishInnerRead(message.id, false, \"Too many concurrent reads\");\n return;\n }\n var binding = reads[message.name];\n if (!binding || typeof binding !== \"object\" || typeof binding.address !== \"string\") {\n finishInnerRead(message.id, false, \"Invalid read binding\");\n return;\n }\n var supplied = message.args;\n if (!supplied || typeof supplied !== \"object\" || Array.isArray(supplied)) {\n finishInnerRead(message.id, false, \"Read arguments must be an object\");\n return;\n }\n var allowed = Array.isArray(binding.viewArgs) ? binding.viewArgs : [];\n var suppliedKeys = Object.keys(supplied);\n for (var i = 0; i < suppliedKeys.length; i++) {\n var key = suppliedKeys[i];\n if (allowed.indexOf(key) === -1) {\n finishInnerRead(message.id, false, \"Undeclared read argument \" + JSON.stringify(key));\n return;\n }\n }\n var args = Object.create(null);\n var fixed = binding.fixedArgs && typeof binding.fixedArgs === \"object\"\n ? binding.fixedArgs\n : {};\n Object.keys(fixed).forEach(function (key) { args[key] = fixed[key]; });\n suppliedKeys.forEach(function (key) { args[key] = supplied[key]; });\n\n var hostId = \"connecta-ui-read-\" + String(++nextHostRequestId);\n pendingHostReads[hostId] = { innerId: message.id };\n activeHostReads++;\n send({\n jsonrpc: \"2.0\",\n id: hostId,\n method: \"tools/call\",\n params: {\n name: \"call_tool\",\n arguments: {\n address: binding.address,\n args: args,\n resultMode: \"value\"\n }\n }\n });\n }\n\n function finishHostRead(message) {\n var pending = pendingHostReads[message.id];\n if (!pending) return false;\n delete pendingHostReads[message.id];\n activeHostReads--;\n if (message.error) {\n finishInnerRead(pending.innerId, false, readError(message.error, \"Host rejected read\"));\n return true;\n }\n var toolResult = message.result;\n if (!toolResult || typeof toolResult !== \"object\") {\n finishInnerRead(pending.innerId, false, \"Host returned an invalid tool result\");\n return true;\n }\n var structured = toolResult.structuredContent;\n if (toolResult.isError || (structured && structured.ok === false)) {\n var detail = structured && structured.error;\n var content = Array.isArray(toolResult.content)\n ? toolResult.content.find(function (block) { return block && block.type === \"text\"; })\n : null;\n finishInnerRead(\n pending.innerId,\n false,\n readError(detail, content && content.text ? content.text : \"Read failed\")\n );\n return true;\n }\n var value = structured && structured.ok === true &&\n Object.prototype.hasOwnProperty.call(structured, \"data\")\n ? structured.data\n : structured !== undefined\n ? structured\n : toolResult;\n finishInnerRead(pending.innerId, true, value);\n return true;\n }\n\n window.addEventListener(\"message\", function (event) {\n var message = event.data;\n if (event.source === view.contentWindow) {\n if (message && message.type === \"connecta/read\") beginInnerRead(message);\n return;\n }\n if (event.source !== host) return;\n if (!message || message.jsonrpc !== \"2.0\") return;\n if (message.id !== undefined && finishHostRead(message)) return;\n if (message.method === \"ui/notifications/tool-result\") {\n render(message.params);\n return;\n }\n if (message.method === \"ui/resource-teardown\") {\n // A host->view request, not a notification: the host waits for\n // this reply before it tears the view down. There is nothing to\n // release, so answer immediately rather than make it time out.\n if (message.id !== undefined && message.id !== null) {\n send({ jsonrpc: \"2.0\", id: message.id, result: {} });\n }\n return;\n }\n // Only a completed handshake earns \"initialized\". A JSON-RPC error\n // response carries the same id, and announcing initialization on one\n // would assert a handshake that never happened.\n if (message.id === initializeId && message.result !== undefined) {\n var capabilities = message.result.hostCapabilities;\n hostCanCallTools = Boolean(capabilities && capabilities.serverTools);\n notify(\"ui/notifications/initialized\", {});\n }\n });\n\n window.addEventListener(\"resize\", reportSize);\n view.addEventListener(\"load\", reportSize);\n\n // Every field here is required by the Apps initialize schema, and a\n // conforming host rejects the request outright when one is missing \u2014\n // which would strand the shell before any tool result arrives.\n send({\n jsonrpc: \"2.0\",\n id: initializeId,\n method: \"ui/initialize\",\n params: {\n appInfo: { name: \"connecta program view\", version: \"1\" },\n appCapabilities: {},\n protocolVersion: \"2026-01-26\"\n }\n });\n reportSize();\n })();\n </script>\n </body>\n</html>\n";
|
|
37
|
+
export declare const PROGRAM_UI_SHELL_HTML = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>connecta program view</title>\n <style>\n html,\n body {\n margin: 0;\n padding: 0;\n background: transparent;\n }\n #program-view {\n display: block;\n width: 100%;\n min-height: 480px;\n border: 0;\n }\n </style>\n </head>\n <body>\n <iframe\n id=\"program-view\"\n title=\"Program-rendered view\"\n sandbox=\"allow-scripts\"\n srcdoc=\"\"\n ></iframe>\n <script>\n (function () {\n \"use strict\";\n // The host frame is the only peer this shell speaks to, in either\n // direction. The payload frame below is sandboxed to scripts alone,\n // with no same-origin escape, and is never handed a reply path:\n // anything it posts fails the source check and is dropped. There is\n // no bridge from program HTML to the host, by construction rather\n // than by validation.\n var host = window.parent;\n var view = document.getElementById(\"program-view\");\n var initializeId = \"connecta-ui-initialize\";\n var lastWidth = 0;\n var lastHeight = 0;\n\n function send(message) {\n if (!host || host === window) return;\n host.postMessage(message, \"*\");\n }\n\n function notify(method, params) {\n send({ jsonrpc: \"2.0\", method: method, params: params });\n }\n\n // Program views are fixed-height by construction. The shell has no\n // bridge to the payload frame \u2014 that is the security posture, not an\n // omission \u2014 so it can never learn the payload's content height, and\n // what it reports here is its own box: the min-height above, unless\n // the host has given it more. Taller content scrolls inside the inner\n // frame rather than growing the view. Raising the min-height is the\n // only lever; a content-height signal would cost the isolation.\n function reportSize() {\n var width = Math.ceil(document.documentElement.clientWidth);\n var height = Math.ceil(document.documentElement.scrollHeight);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width;\n lastHeight = height;\n notify(\"ui/notifications/size-changed\", {\n width: width,\n height: height\n });\n }\n\n function payloadHtml(result) {\n if (!result || typeof result !== \"object\") return null;\n var meta = result._meta;\n if (!meta || typeof meta !== \"object\") return null;\n var payload = meta[\"connecta/ui\"];\n if (!payload || typeof payload !== \"object\") return null;\n var html = payload.html;\n return typeof html === \"string\" && html.length > 0 ? html : null;\n }\n\n function render(params) {\n var html =\n payloadHtml(params) ||\n payloadHtml(params && params.result) ||\n payloadHtml(params && params.toolResult);\n if (html === null) return;\n view.srcdoc = html;\n reportSize();\n }\n\n window.addEventListener(\"message\", function (event) {\n if (event.source !== host) return;\n var message = event.data;\n if (!message || message.jsonrpc !== \"2.0\") return;\n if (message.method === \"ui/notifications/tool-result\") {\n render(message.params);\n return;\n }\n if (message.method === \"ui/resource-teardown\") {\n // A host->view request, not a notification: the host waits for\n // this reply before it tears the view down. There is nothing to\n // release, so answer immediately rather than make it time out.\n if (message.id !== undefined && message.id !== null) {\n send({ jsonrpc: \"2.0\", id: message.id, result: {} });\n }\n return;\n }\n // Only a completed handshake earns \"initialized\". A JSON-RPC error\n // response carries the same id, and announcing initialization on one\n // would assert a handshake that never happened.\n if (message.id === initializeId && message.result !== undefined) {\n notify(\"ui/notifications/initialized\", {});\n }\n });\n\n window.addEventListener(\"resize\", reportSize);\n view.addEventListener(\"load\", reportSize);\n\n // Every field here is required by the Apps initialize schema, and a\n // conforming host rejects the request outright when one is missing \u2014\n // which would strand the shell before any tool result arrives.\n send({\n jsonrpc: \"2.0\",\n id: initializeId,\n method: \"ui/initialize\",\n params: {\n appInfo: { name: \"connecta program view\", version: \"1\" },\n appCapabilities: {},\n protocolVersion: \"2026-01-26\"\n }\n });\n reportSize();\n })();\n </script>\n </body>\n</html>\n";
|
package/dist/apps-shell.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A build-time string constant, not a file read at startup: the core is
|
|
5
5
|
* Web-API-only so it runs unchanged on Workers, and the same bytes have to
|
|
6
|
-
* serve everywhere.
|
|
7
|
-
* inside a nested `srcdoc` frame
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* serve everywhere. The shell is display-only: it renders whatever HTML a
|
|
7
|
+
* program handed `connecta.ui` inside a nested `srcdoc` frame and forwards no
|
|
8
|
+
* channel back from that frame to the host, so program-authored markup is
|
|
9
|
+
* inert beyond its own pixels.
|
|
10
10
|
*
|
|
11
11
|
* The address carries a version segment because hosts are permitted to
|
|
12
12
|
* prefetch and cache templates by URI: change these bytes, bump the version.
|
|
13
13
|
*/
|
|
14
14
|
/** The only `ui://` URI in the system. No program input reaches it. */
|
|
15
|
-
export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/
|
|
15
|
+
export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v3";
|
|
16
16
|
/** The mimeType the Apps spec requires of an HTML template. */
|
|
17
17
|
export const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
|
|
18
18
|
/**
|
|
@@ -29,12 +29,10 @@ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
|
|
|
29
29
|
* Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
|
|
30
30
|
* `ui/notifications/tool-result`, `ui/notifications/size-changed`,
|
|
31
31
|
* `ui/resource-teardown`), lifts `_meta["connecta/ui"].html` out of the
|
|
32
|
-
* delivered tool result, and puts it in a frame.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
|
|
37
|
-
* no direct network.
|
|
32
|
+
* delivered tool result, and puts it in a frame. It declares no CSP domains,
|
|
33
|
+
* so the host applies its restrictive default and the `srcdoc` frame inherits
|
|
34
|
+
* `default-src 'none'`. The payload gets scripts and local interactivity, and
|
|
35
|
+
* no network.
|
|
38
36
|
*/
|
|
39
37
|
export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
|
|
40
38
|
<html lang="en">
|
|
@@ -67,21 +65,17 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
|
|
|
67
65
|
<script>
|
|
68
66
|
(function () {
|
|
69
67
|
"use strict";
|
|
70
|
-
// The
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
68
|
+
// The host frame is the only peer this shell speaks to, in either
|
|
69
|
+
// direction. The payload frame below is sandboxed to scripts alone,
|
|
70
|
+
// with no same-origin escape, and is never handed a reply path:
|
|
71
|
+
// anything it posts fails the source check and is dropped. There is
|
|
72
|
+
// no bridge from program HTML to the host, by construction rather
|
|
73
|
+
// than by validation.
|
|
74
74
|
var host = window.parent;
|
|
75
75
|
var view = document.getElementById("program-view");
|
|
76
76
|
var initializeId = "connecta-ui-initialize";
|
|
77
77
|
var lastWidth = 0;
|
|
78
78
|
var lastHeight = 0;
|
|
79
|
-
var reads = null;
|
|
80
|
-
var hostCanCallTools = false;
|
|
81
|
-
var nextHostRequestId = 0;
|
|
82
|
-
var pendingHostReads = Object.create(null);
|
|
83
|
-
var activeHostReads = 0;
|
|
84
|
-
var maxActiveHostReads = 8;
|
|
85
79
|
|
|
86
80
|
function send(message) {
|
|
87
81
|
if (!host || host === window) return;
|
|
@@ -93,8 +87,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
|
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
// Program views are fixed-height by construction. The shell has no
|
|
96
|
-
//
|
|
97
|
-
// the payload's content height, and
|
|
90
|
+
// bridge to the payload frame — that is the security posture, not an
|
|
91
|
+
// omission — so it can never learn the payload's content height, and
|
|
98
92
|
// what it reports here is its own box: the min-height above, unless
|
|
99
93
|
// the host has given it more. Taller content scrolls inside the inner
|
|
100
94
|
// frame rather than growing the view. Raising the min-height is the
|
|
@@ -111,213 +105,30 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
|
|
|
111
105
|
});
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
function
|
|
108
|
+
function payloadHtml(result) {
|
|
115
109
|
if (!result || typeof result !== "object") return null;
|
|
116
110
|
var meta = result._meta;
|
|
117
111
|
if (!meta || typeof meta !== "object") return null;
|
|
118
|
-
var
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
: null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// This function is serialized into the opaque-origin payload frame.
|
|
126
|
-
// It knows no addresses and has no host channel of its own: one named
|
|
127
|
-
// read request goes to the trusted outer shell and one correlated
|
|
128
|
-
// result comes back.
|
|
129
|
-
function payloadReadBridge() {
|
|
130
|
-
"use strict";
|
|
131
|
-
var pending = Object.create(null);
|
|
132
|
-
var nextId = 0;
|
|
133
|
-
|
|
134
|
-
function read(name, args) {
|
|
135
|
-
return new Promise(function (resolve, reject) {
|
|
136
|
-
var id = String(++nextId);
|
|
137
|
-
pending[id] = { resolve: resolve, reject: reject };
|
|
138
|
-
try {
|
|
139
|
-
window.parent.postMessage({
|
|
140
|
-
type: "connecta/read",
|
|
141
|
-
id: id,
|
|
142
|
-
name: name,
|
|
143
|
-
args: args === undefined ? {} : args
|
|
144
|
-
}, "*");
|
|
145
|
-
} catch (error) {
|
|
146
|
-
delete pending[id];
|
|
147
|
-
reject(error);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
Object.defineProperty(globalThis, "connecta", {
|
|
153
|
-
value: Object.freeze({ read: read }),
|
|
154
|
-
configurable: false,
|
|
155
|
-
enumerable: true,
|
|
156
|
-
writable: false
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
window.addEventListener("message", function (event) {
|
|
160
|
-
if (event.source !== window.parent) return;
|
|
161
|
-
var message = event.data;
|
|
162
|
-
if (!message || message.type !== "connecta/read-result") return;
|
|
163
|
-
var waiter = pending[message.id];
|
|
164
|
-
if (!waiter) return;
|
|
165
|
-
delete pending[message.id];
|
|
166
|
-
if (message.ok) waiter.resolve(message.value);
|
|
167
|
-
else waiter.reject(new Error(message.error || "Read failed"));
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function htmlWithReadBridge(html) {
|
|
172
|
-
var script =
|
|
173
|
-
"<scr" + "ipt>(" + payloadReadBridge.toString() + ")();</scr" + "ipt>";
|
|
174
|
-
var head = /<head(?:\\s[^>]*)?>/i.exec(html);
|
|
175
|
-
if (head) {
|
|
176
|
-
var at = (head.index || 0) + head[0].length;
|
|
177
|
-
return html.slice(0, at) + script + html.slice(at);
|
|
178
|
-
}
|
|
179
|
-
var document = /<html(?:\\s[^>]*)?>/i.exec(html);
|
|
180
|
-
if (document) {
|
|
181
|
-
var afterHtml = (document.index || 0) + document[0].length;
|
|
182
|
-
return html.slice(0, afterHtml) + "<head>" + script + "</head>" + html.slice(afterHtml);
|
|
183
|
-
}
|
|
184
|
-
return script + html;
|
|
112
|
+
var payload = meta["connecta/ui"];
|
|
113
|
+
if (!payload || typeof payload !== "object") return null;
|
|
114
|
+
var html = payload.html;
|
|
115
|
+
return typeof html === "string" && html.length > 0 ? html : null;
|
|
185
116
|
}
|
|
186
117
|
|
|
187
118
|
function render(params) {
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
? value.reads
|
|
195
|
-
: null;
|
|
196
|
-
view.srcdoc = reads
|
|
197
|
-
? htmlWithReadBridge(value.html)
|
|
198
|
-
: value.html;
|
|
119
|
+
var html =
|
|
120
|
+
payloadHtml(params) ||
|
|
121
|
+
payloadHtml(params && params.result) ||
|
|
122
|
+
payloadHtml(params && params.toolResult);
|
|
123
|
+
if (html === null) return;
|
|
124
|
+
view.srcdoc = html;
|
|
199
125
|
reportSize();
|
|
200
126
|
}
|
|
201
127
|
|
|
202
|
-
function readError(message, fallback) {
|
|
203
|
-
if (message && typeof message.message === "string") return message.message;
|
|
204
|
-
if (message && message.data && typeof message.data.message === "string") {
|
|
205
|
-
return message.data.message;
|
|
206
|
-
}
|
|
207
|
-
return fallback;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function finishInnerRead(innerId, ok, value) {
|
|
211
|
-
if (!view.contentWindow) return;
|
|
212
|
-
view.contentWindow.postMessage(ok
|
|
213
|
-
? { type: "connecta/read-result", id: innerId, ok: true, value: value }
|
|
214
|
-
: { type: "connecta/read-result", id: innerId, ok: false, error: value }, "*");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function beginInnerRead(message) {
|
|
218
|
-
if (!hostCanCallTools) {
|
|
219
|
-
finishInnerRead(message && message.id, false, "This host does not support app-initiated server tool calls");
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (!message || typeof message.id !== "string" || typeof message.name !== "string") return;
|
|
223
|
-
if (!reads || !Object.prototype.hasOwnProperty.call(reads, message.name)) {
|
|
224
|
-
finishInnerRead(message.id, false, "Unknown read binding");
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (activeHostReads >= maxActiveHostReads) {
|
|
228
|
-
finishInnerRead(message.id, false, "Too many concurrent reads");
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
var binding = reads[message.name];
|
|
232
|
-
if (!binding || typeof binding !== "object" || typeof binding.address !== "string") {
|
|
233
|
-
finishInnerRead(message.id, false, "Invalid read binding");
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
var supplied = message.args;
|
|
237
|
-
if (!supplied || typeof supplied !== "object" || Array.isArray(supplied)) {
|
|
238
|
-
finishInnerRead(message.id, false, "Read arguments must be an object");
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
var allowed = Array.isArray(binding.viewArgs) ? binding.viewArgs : [];
|
|
242
|
-
var suppliedKeys = Object.keys(supplied);
|
|
243
|
-
for (var i = 0; i < suppliedKeys.length; i++) {
|
|
244
|
-
var key = suppliedKeys[i];
|
|
245
|
-
if (allowed.indexOf(key) === -1) {
|
|
246
|
-
finishInnerRead(message.id, false, "Undeclared read argument " + JSON.stringify(key));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
var args = Object.create(null);
|
|
251
|
-
var fixed = binding.fixedArgs && typeof binding.fixedArgs === "object"
|
|
252
|
-
? binding.fixedArgs
|
|
253
|
-
: {};
|
|
254
|
-
Object.keys(fixed).forEach(function (key) { args[key] = fixed[key]; });
|
|
255
|
-
suppliedKeys.forEach(function (key) { args[key] = supplied[key]; });
|
|
256
|
-
|
|
257
|
-
var hostId = "connecta-ui-read-" + String(++nextHostRequestId);
|
|
258
|
-
pendingHostReads[hostId] = { innerId: message.id };
|
|
259
|
-
activeHostReads++;
|
|
260
|
-
send({
|
|
261
|
-
jsonrpc: "2.0",
|
|
262
|
-
id: hostId,
|
|
263
|
-
method: "tools/call",
|
|
264
|
-
params: {
|
|
265
|
-
name: "call_tool",
|
|
266
|
-
arguments: {
|
|
267
|
-
address: binding.address,
|
|
268
|
-
args: args,
|
|
269
|
-
resultMode: "value"
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function finishHostRead(message) {
|
|
276
|
-
var pending = pendingHostReads[message.id];
|
|
277
|
-
if (!pending) return false;
|
|
278
|
-
delete pendingHostReads[message.id];
|
|
279
|
-
activeHostReads--;
|
|
280
|
-
if (message.error) {
|
|
281
|
-
finishInnerRead(pending.innerId, false, readError(message.error, "Host rejected read"));
|
|
282
|
-
return true;
|
|
283
|
-
}
|
|
284
|
-
var toolResult = message.result;
|
|
285
|
-
if (!toolResult || typeof toolResult !== "object") {
|
|
286
|
-
finishInnerRead(pending.innerId, false, "Host returned an invalid tool result");
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
var structured = toolResult.structuredContent;
|
|
290
|
-
if (toolResult.isError || (structured && structured.ok === false)) {
|
|
291
|
-
var detail = structured && structured.error;
|
|
292
|
-
var content = Array.isArray(toolResult.content)
|
|
293
|
-
? toolResult.content.find(function (block) { return block && block.type === "text"; })
|
|
294
|
-
: null;
|
|
295
|
-
finishInnerRead(
|
|
296
|
-
pending.innerId,
|
|
297
|
-
false,
|
|
298
|
-
readError(detail, content && content.text ? content.text : "Read failed")
|
|
299
|
-
);
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
var value = structured && structured.ok === true &&
|
|
303
|
-
Object.prototype.hasOwnProperty.call(structured, "data")
|
|
304
|
-
? structured.data
|
|
305
|
-
: structured !== undefined
|
|
306
|
-
? structured
|
|
307
|
-
: toolResult;
|
|
308
|
-
finishInnerRead(pending.innerId, true, value);
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
128
|
window.addEventListener("message", function (event) {
|
|
313
|
-
var message = event.data;
|
|
314
|
-
if (event.source === view.contentWindow) {
|
|
315
|
-
if (message && message.type === "connecta/read") beginInnerRead(message);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
129
|
if (event.source !== host) return;
|
|
130
|
+
var message = event.data;
|
|
319
131
|
if (!message || message.jsonrpc !== "2.0") return;
|
|
320
|
-
if (message.id !== undefined && finishHostRead(message)) return;
|
|
321
132
|
if (message.method === "ui/notifications/tool-result") {
|
|
322
133
|
render(message.params);
|
|
323
134
|
return;
|
|
@@ -335,8 +146,6 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
|
|
|
335
146
|
// response carries the same id, and announcing initialization on one
|
|
336
147
|
// would assert a handshake that never happened.
|
|
337
148
|
if (message.id === initializeId && message.result !== undefined) {
|
|
338
|
-
var capabilities = message.result.hostCapabilities;
|
|
339
|
-
hostCanCallTools = Boolean(capabilities && capabilities.serverTools);
|
|
340
149
|
notify("ui/notifications/initialized", {});
|
|
341
150
|
}
|
|
342
151
|
});
|
package/dist/execute.d.ts
CHANGED
|
@@ -91,14 +91,8 @@ export type EmittedBlock = {
|
|
|
91
91
|
data: string;
|
|
92
92
|
mimeType: string;
|
|
93
93
|
};
|
|
94
|
-
interface UiReadBinding {
|
|
95
|
-
address: string;
|
|
96
|
-
fixedArgs: Record<string, unknown>;
|
|
97
|
-
viewArgs: string[];
|
|
98
|
-
}
|
|
99
94
|
interface UiPayload {
|
|
100
95
|
html: string;
|
|
101
|
-
reads?: Record<string, UiReadBinding>;
|
|
102
96
|
}
|
|
103
97
|
/**
|
|
104
98
|
* Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
|
|
@@ -136,7 +130,6 @@ export declare class EmitCollector {
|
|
|
136
130
|
* complaint about its type would send the author to fix the wrong thing.
|
|
137
131
|
*/
|
|
138
132
|
acceptUi(...values: unknown[]): void;
|
|
139
|
-
acceptValidatedUi(payload: UiPayload): void;
|
|
140
133
|
private assertUiVacant;
|
|
141
134
|
private acceptUiPayload;
|
|
142
135
|
}
|