@zackbart/connecta 0.12.1 → 0.13.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/README.md +4 -1
  3. package/dist/apps-shell.d.ts +13 -11
  4. package/dist/apps-shell.d.ts.map +1 -1
  5. package/dist/apps-shell.js +221 -30
  6. package/dist/apps-shell.js.map +1 -1
  7. package/dist/catalog-service.d.ts +41 -0
  8. package/dist/catalog-service.d.ts.map +1 -1
  9. package/dist/catalog-service.js +94 -5
  10. package/dist/catalog-service.js.map +1 -1
  11. package/dist/connectors/api.d.ts +5 -4
  12. package/dist/connectors/api.d.ts.map +1 -1
  13. package/dist/connectors/api.js.map +1 -1
  14. package/dist/connectors/remote-mcp.d.ts +5 -4
  15. package/dist/connectors/remote-mcp.d.ts.map +1 -1
  16. package/dist/connectors/remote-mcp.js.map +1 -1
  17. package/dist/execute.d.ts +12 -4
  18. package/dist/execute.d.ts.map +1 -1
  19. package/dist/execute.js +142 -20
  20. package/dist/execute.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/meta-tools.d.ts.map +1 -1
  25. package/dist/meta-tools.js +14 -4
  26. package/dist/meta-tools.js.map +1 -1
  27. package/dist/providers/mixpanel.d.ts +21 -0
  28. package/dist/providers/mixpanel.d.ts.map +1 -0
  29. package/dist/providers/mixpanel.js +183 -0
  30. package/dist/providers/mixpanel.js.map +1 -0
  31. package/dist/skills.d.ts +7 -9
  32. package/dist/skills.d.ts.map +1 -1
  33. package/dist/skills.js +60 -25
  34. package/dist/skills.js.map +1 -1
  35. package/dist/types.d.ts +26 -6
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/documentation/code-mode.md +32 -20
  40. package/documentation/connectors.md +116 -4
  41. package/documentation/mcp-ui-design.md +8 -8
  42. package/documentation/meta-tools.md +80 -8
  43. package/documentation/mixpanel.md +72 -0
  44. package/documentation/program-ui-read-calls.md +213 -0
  45. package/ethos.md +10 -4
  46. package/package.json +5 -1
  47. package/src/apps-shell.ts +221 -30
  48. package/src/catalog-service.ts +139 -4
  49. package/src/connectors/api.ts +5 -3
  50. package/src/connectors/remote-mcp.ts +5 -3
  51. package/src/execute.ts +215 -21
  52. package/src/index.ts +1 -0
  53. package/src/meta-tools.ts +19 -4
  54. package/src/providers/mixpanel.ts +220 -0
  55. package/src/skills.ts +66 -24
  56. package/src/types.ts +27 -6
  57. package/src/version.ts +1 -1
  58. package/templates/node/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,179 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.13.0 — 2026-08-03
6
+
7
+ Everything an agent reads before it calls anything got more selective. Connecta
8
+ ships its first maintained prebuilt connection — `mixpanel()` behind
9
+ `./providers/mixpanel`, one import returning one ordinary `Connector` with the
10
+ provider's endpoint, region, auth, and rate-limit defaults already right — and
11
+ the ethos now names a maintained prebuilt connection the preferred authoring
12
+ path, with `remoteMcp()` and `api()` staying equal first-class primitives for
13
+ everything nobody maintains. Connector guides became structured: a guide can
14
+ declare a bounded `summary` and mark itself `required`, and discovery carries
15
+ `guideSummary` with `guideRequired`/`guideRequiredReasons`, so an agent fetches
16
+ a guide when it changes the call and skips it when a complete read-only schema
17
+ already says everything. A connector-scoped search whose catalog is down now
18
+ returns a typed `catalogError` instead of advice to retry later. And the tool
19
+ descriptions stopped inviting the two failures the new cold-agent benchmark
20
+ kept catching: a top-level search that duplicates the one the program was about
21
+ to run, and a program guessing at a result shape it never read.
22
+
23
+ No API breaks. `usageGuide` still accepts a plain markdown string and means
24
+ exactly what it did; every new discovery field is additive and absent unless
25
+ earned. What changed under existing deployments is text, and it is worth
26
+ knowing about: the served tool descriptions and the MCP `instructions` string
27
+ are rewritten, and `skills({})` now summarizes a connector guide from its first
28
+ body line rather than its heading — a guide opening `# Acme` that used to list
29
+ as "Acme" now lists as the sentence beneath it. The built-in usage skill also
30
+ grew, because the per-connector guides section is now appended unconditionally
31
+ rather than only where a guide exists; that keeps the shared guide
32
+ byte-identical across every deployment, so an agent that read it once in a task
33
+ never needs a second local copy, at the cost of one paragraph in deployments
34
+ with no guides. Upgrading is a version bump and nothing else — there is no new
35
+ configuration, no new dependency, and no behavior a deployment must opt into
36
+ (#294, #295, #296, #297, PRs #298–#302).
37
+
38
+ ### Added
39
+
40
+ - **A maintained Mixpanel connection at `./providers/mixpanel`.**
41
+ `mixpanel(id, { purpose, region?, auth?, instructions?, title?,
42
+ maxResultBytes? })` proxies Mixpanel's hosted MCP server with the endpoint
43
+ chosen by data residency (`us`, `eu`, `in`, default `us`), OAuth by default
44
+ and static headers for service accounts, HTTPS required, and a rolling
45
+ 600-calls-per-hour admission budget matching the provider's published limit.
46
+ It carries a maintained usage guide — start at `Get-Projects`, then
47
+ `Get-Business-Context`; discover names instead of guessing spellings; fetch
48
+ `Get-Query-Schema` before `Run-Query` — to which a deployment may append its
49
+ own account instructions. The subpath is an optional import, not a
50
+ dependency: nothing new installs with core.
51
+ - **Fill-in-only vetted safety annotations.** The connection classifies 63
52
+ Mixpanel tools — 35 read-only, 28 writes split into additive creates and
53
+ destructive edits — but only where the downstream is silent. An explicit
54
+ `destructiveHint: true` or `readOnlyHint: false` from Mixpanel on an
55
+ allowlisted read name wins, because that is the downstream saying this
56
+ release's allowlist is wrong. Tools this release has never seen fail closed
57
+ to approval-visible rather than being assumed safe.
58
+ - **`ConnectorUsageGuide`.** A connector may now declare
59
+ `usageGuide: { content, summary?, required? }` instead of a bare string.
60
+ `summary` is the bounded line discovery shows; `required: true` says correct
61
+ use always depends on conventions no tool schema can carry. Mutations and
62
+ truncated schemas already require review and do not need the flag.
63
+ - **`guideSummary`, `guideRequired`, and `guideRequiredReasons` on discovery.**
64
+ `search_tools`, `connecta.search`, and `connecta.describe` now say what a
65
+ guide covers and whether it must be read first, with the reason named:
66
+ `connector_required` and `approval_required` stand however far a schema is
67
+ expanded, while `schema_truncated` clears once describe returns the exact
68
+ shape. A connector-scoped search that matches nothing still surfaces the
69
+ connector's guide, so an agent that searched the wrong terms learns the
70
+ vocabulary instead of concluding the connector is empty.
71
+ - **A scoped `catalogError`.** A search explicitly scoped to one connector
72
+ whose catalog is unavailable now returns the classified failure — `code`,
73
+ bounded `message`, `retryable`, and `retryAfterMs` when known — so an agent
74
+ can tell a transient outage from one an operator has to clear. Exactly four
75
+ fields, pinned by a test: an unscoped search still gets only
76
+ `unavailableConnectorCount`, because one connector's failure is not another
77
+ search's context.
78
+
79
+ ### Changed
80
+
81
+ - **Routing guidance no longer invites redundant discovery.** Top-level
82
+ `search_tools` is now reserved for a single unreduced read or for
83
+ write-capable work; anything involving reduction, dependent steps, loops, or
84
+ joins is one `execute_code` program that searches and calls inside the run.
85
+ Search guidance also tells an agent to scope to an obvious connector id and
86
+ to require purpose, input, truncation, safety, and output fit rather than
87
+ taking the first lexical match. Measured against the shipped text over five
88
+ repetitions, route compliance went from 9/30 to 25/30.
89
+ - **`call_destructive_tool` carries the guide note.** Destructive multi-step
90
+ work keeps its route through top-level discovery — `execute_code` admits only
91
+ read-only tools — and its description now says to inspect the address and
92
+ fetch any guide it names before a consequential call.
93
+ - **The ethos prefers prebuilt connections, and refuses a registry.** The
94
+ decisions table accepts prebuilt connections as the preferred authoring path
95
+ *when connecta maintains one*, fenced: exactly one ordinary `Connector` with
96
+ no extra privileges, never a bundle or preset, tools hand-written or proxied
97
+ rather than generated from a schema document, and vetted annotations that
98
+ only fill in downstream silence. A provider registry or integration
99
+ marketplace is refused outright — prebuilt connections are imports, not
100
+ listings, and discovery happens in documentation, never at runtime.
101
+ - **The usage skill is byte-identical everywhere.** The per-connector guides
102
+ section is appended unconditionally and rewritten to route on
103
+ `guideRequired`/`guideSummary` rather than telling agents to read every
104
+ guide before first use. Guide-free deployments still pay no fixed
105
+ tool-description cost — the conditional notes in the meta-tool descriptions
106
+ remain absent.
107
+ - **Guide summaries prefer substance over headings.** `skills({})` reads a
108
+ guide's first meaningful body line, falling back to the heading and then to
109
+ the connector description, so a listing describes what a guide says rather
110
+ than what it is titled.
111
+
112
+ ### Fixed
113
+
114
+ - **The Mixpanel connection could not have booted.** Its call-admission rule
115
+ paired a budget with `retryAfterMs` — a queue setting without a queue, which
116
+ the admission controller refuses at construction. No suite caught it because
117
+ every suite stubbed remote-MCP before the registry saw the connector; a new
118
+ suite now boots two accounts through the real `createConnecta` with `fetch`
119
+ rigged to throw, proving the boot and the per-account address, catalog,
120
+ storage, credential, and budget namespaces.
121
+ - **Tool descriptions claimed a waiver describe already performs.** Three
122
+ descriptions said schema expansion never clears a guide requirement while
123
+ `describe` cleared `schema_truncated` on every call. The text moved to the
124
+ behavior rather than the reverse.
125
+ - **Package-surface guards derive from `src/providers/`** instead of naming
126
+ `mixpanel.ts`, so the next provider fails them only for a real reason.
127
+
128
+ ### Internal
129
+
130
+ - **A cold-agent benchmark lane in `eval/`.** Fourteen cases across two lanes —
131
+ eight measuring whether an agent meeting a connector cold discovers, routes,
132
+ and recovers, six measuring route compliance for the description rewrite —
133
+ with a comparator that refuses to compare runs whose harness, scoring, or
134
+ sandbox fingerprints differ, and reports a `productSha256` over `src/**`. A
135
+ baseline and a candidate cut from one working tree record the same commit and
136
+ the same dirty flag, and only the fingerprint says whether the candidate
137
+ measured changed code. It also reports host-routing probes separately from
138
+ foreign calls, so a contaminated run announces itself instead of reading as a
139
+ product regression. `eval/` is not in the published `files` list; deployments
140
+ never see it.
141
+
142
+ ## 0.12.2 — 2026-08-02
143
+
144
+ Rendered programs can now bind a small, explicit set of read-only connector
145
+ calls into their view. `connecta.ui(html, { reads })` validates every binding
146
+ against the request-local catalog before accepting the UI, and the trusted
147
+ Apps shell exposes only those names and declared view arguments through
148
+ `connecta.read`. The program markup never receives connector addresses or a
149
+ general tool channel, and mutation-capable tools remain unavailable. Existing
150
+ `connecta.ui(html)` calls are byte-for-byte unchanged and remain display-only;
151
+ deployments whose views do not need refresh, pagination, or drill-down reads
152
+ can ignore this release. No deployment configuration changes are required
153
+ (#287, #289, PR #290).
154
+
155
+ ### Added
156
+
157
+ - **Bounded read bindings for rendered views.** Programs may bind 1–32 names
158
+ to catalog addresses proven read-only, with optional fixed arguments and an
159
+ allowlist of view-supplied arguments. The manifest shares the existing
160
+ emitted-byte budget and binding does not dispatch a call or spend the
161
+ program's host-call budget.
162
+ - **A narrow `connecta.read(name, args)` view API.** The trusted outer shell
163
+ translates bound names into ordinary `call_tool` requests, reusing its
164
+ current authorization, catalog, and safety checks. It rejects unknown names,
165
+ undeclared arguments, fixed-argument overrides, and more than eight
166
+ concurrent reads.
167
+
168
+ ### Changed
169
+
170
+ - **The program UI resource advances to `/v2`.** The new shell requires the
171
+ host's MCP Apps server-tools capability and makes only `call_tool` visible to
172
+ the app; the other six meta-tools remain model-only. Display-only views do
173
+ not receive the read bridge.
174
+ - **The UI security contract is explicit.** The new design record defines the
175
+ data flow, threat model, validation rules, executor parity, and the boundary
176
+ between accepted reads and still-gated mutations.
177
+
5
178
  ## 0.12.1 — 2026-08-01
6
179
 
7
180
  Instructions, not behavior. Calling-side feedback and a first-hand run of the
package/README.md CHANGED
@@ -72,7 +72,10 @@ For an agent setting this up, the contract is:
72
72
  2. Keep the required `executor: quickJsExecutor()` configuration; without an
73
73
  executor the deployment refuses to boot.
74
74
  3. Keep secrets in environment variables or a secret store, never source.
75
- 4. Add code only for deliberate `api()` connectors.
75
+ 4. Add connectors explicitly: import a maintained prebuilt provider
76
+ constructor when one exists, otherwise write a deliberate `remoteMcp()` or
77
+ `api()` connector. There is no registry to browse and nothing registers
78
+ itself.
76
79
  5. Run `npm run typecheck`, start the server, and run
77
80
  `CONNECTA_TOKEN=... npm run doctor`. Doctor checks health, the executor, and
78
81
  the exact seven-tool model-facing surface, then executes a harmless sandbox
@@ -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. 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.
6
+ * serve everywhere. It renders whatever HTML a program handed `connecta.ui`
7
+ * inside a nested `srcdoc` frame. The one-argument form forwards no channel;
8
+ * an explicitly bound view gets only named read calls through the trusted
9
+ * shell, never a raw host channel.
10
10
  *
11
11
  * The address carries a version segment because hosts are permitted to
12
- * prefetch and cache templates by URI: change these bytes, bump `v1`.
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/v1";
15
+ export declare const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v2";
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,10 +29,12 @@ 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. 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,
35
- * and no network.
32
+ * delivered tool result, and puts it in a frame. An optional read manifest
33
+ * installs one narrow `connecta.read(name, args)` bridge in that inner frame;
34
+ * the outer shell maps declared names to the existing `call_tool` meta-tool.
35
+ * It declares no CSP domains, so the host applies its restrictive default and
36
+ * the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
37
+ * no direct network.
36
38
  */
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";
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";
38
40
  //# sourceMappingURL=apps-shell.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"apps-shell.d.ts","sourceRoot":"","sources":["../src/apps-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAErE,+DAA+D;AAC/D,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,ssKAyIjC,CAAC"}
1
+ {"version":3,"file":"apps-shell.d.ts","sourceRoot":"","sources":["../src/apps-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,gCAAgC,CAAC;AAErE,+DAA+D;AAC/D,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,+BAA+B,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,07ZAsUjC,CAAC"}
@@ -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. 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.
6
+ * serve everywhere. It renders whatever HTML a program handed `connecta.ui`
7
+ * inside a nested `srcdoc` frame. The one-argument form forwards no channel;
8
+ * an explicitly bound view gets only named read calls through the trusted
9
+ * shell, never a raw host channel.
10
10
  *
11
11
  * The address carries a version segment because hosts are permitted to
12
- * prefetch and cache templates by URI: change these bytes, bump `v1`.
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/v1";
15
+ export const PROGRAM_UI_RESOURCE_URI = "ui://connecta/program-ui/v2";
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,10 +29,12 @@ 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. 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,
35
- * and no network.
32
+ * delivered tool result, and puts it in a frame. An optional read manifest
33
+ * installs one narrow `connecta.read(name, args)` bridge in that inner frame;
34
+ * the outer shell maps declared names to the existing `call_tool` meta-tool.
35
+ * It declares no CSP domains, so the host applies its restrictive default and
36
+ * the `srcdoc` frame inherits `default-src 'none'` — program markup still gets
37
+ * no direct network.
36
38
  */
37
39
  export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
38
40
  <html lang="en">
@@ -65,17 +67,21 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
65
67
  <script>
66
68
  (function () {
67
69
  "use strict";
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.
70
+ // The outer shell is the only host peer. The payload frame is
71
+ // sandboxed to scripts alone, with no same-origin escape. Its one
72
+ // optional message dialect is handled below and translated into
73
+ // bounded call_tool requests; raw JSON-RPC is never forwarded.
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;
79
85
 
80
86
  function send(message) {
81
87
  if (!host || host === window) return;
@@ -87,8 +93,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
87
93
  }
88
94
 
89
95
  // Program views are fixed-height by construction. The shell has no
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
96
+ // content-height bridge to the payload frame, so it can never learn
97
+ // the payload's content height, and
92
98
  // what it reports here is its own box: the min-height above, unless
93
99
  // the host has given it more. Taller content scrolls inside the inner
94
100
  // frame rather than growing the view. Raising the min-height is the
@@ -105,30 +111,213 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
105
111
  });
106
112
  }
107
113
 
108
- function payloadHtml(result) {
114
+ function payload(result) {
109
115
  if (!result || typeof result !== "object") return null;
110
116
  var meta = result._meta;
111
117
  if (!meta || typeof meta !== "object") return null;
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;
118
+ var value = meta["connecta/ui"];
119
+ if (!value || typeof value !== "object") return null;
120
+ return typeof value.html === "string" && value.html.length > 0
121
+ ? value
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;
116
185
  }
117
186
 
118
187
  function render(params) {
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;
188
+ var value =
189
+ payload(params) ||
190
+ payload(params && params.result) ||
191
+ payload(params && params.toolResult);
192
+ if (value === null) return;
193
+ reads = value.reads && typeof value.reads === "object"
194
+ ? value.reads
195
+ : null;
196
+ view.srcdoc = reads
197
+ ? htmlWithReadBridge(value.html)
198
+ : value.html;
125
199
  reportSize();
126
200
  }
127
201
 
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
+
128
312
  window.addEventListener("message", function (event) {
129
- if (event.source !== host) return;
130
313
  var message = event.data;
314
+ if (event.source === view.contentWindow) {
315
+ if (message && message.type === "connecta/read") beginInnerRead(message);
316
+ return;
317
+ }
318
+ if (event.source !== host) return;
131
319
  if (!message || message.jsonrpc !== "2.0") return;
320
+ if (message.id !== undefined && finishHostRead(message)) return;
132
321
  if (message.method === "ui/notifications/tool-result") {
133
322
  render(message.params);
134
323
  return;
@@ -146,6 +335,8 @@ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
146
335
  // response carries the same id, and announcing initialization on one
147
336
  // would assert a handshake that never happened.
148
337
  if (message.id === initializeId && message.result !== undefined) {
338
+ var capabilities = message.result.hostCapabilities;
339
+ hostCanCallTools = Boolean(capabilities && capabilities.serverTools);
149
340
  notify("ui/notifications/initialized", {});
150
341
  }
151
342
  });
@@ -1 +1 @@
1
- {"version":3,"file":"apps-shell.js","sourceRoot":"","sources":["../src/apps-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,MAAM,CAAC,MAAM,uBAAuB,GAAG,6BAA6B,CAAC;AAErE,+DAA+D;AAC/D,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAC;AAEjD,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyIpC,CAAC"}
1
+ {"version":3,"file":"apps-shell.js","sourceRoot":"","sources":["../src/apps-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,uEAAuE;AACvE,MAAM,CAAC,MAAM,uBAAuB,GAAG,6BAA6B,CAAC;AAErE,+DAA+D;AAC/D,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,aAAa,CAAC;AAEjD,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,4BAA4B,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsUpC,CAAC"}