@zackbart/connecta 0.11.0 → 0.12.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 CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.12.0 — 2026-08-01
6
+
7
+ Programs gained a *view*. `execute_code` code can now call `connecta.ui(html)`
8
+ to hand the client one rendered MCP Apps view of the run — the thing
9
+ `connecta.emit` could not do, which is give a human something to look at while
10
+ the model keeps its cheap textual summary. Programs supply HTML content and
11
+ nothing else: the only `ui://` URI in the system is connecta's build-time shell,
12
+ so nothing a client could dereference is derived from anything a program said,
13
+ and the shell forwards no channel from the program's markup back to the host.
14
+ The channel is additive — a program that never calls `connecta.ui` produces the
15
+ byte-for-byte prior response, no executor changed to carry it, and there is no
16
+ new budget knob. Deployments do gain a `resources` capability and one extension
17
+ declaration, both of which the design requires before any host will render.
18
+ The design record is `documentation/mcp-ui-design.md` (#266); the contract is
19
+ `code-mode.md`'s "Rendered output" clauses (#277).
20
+
21
+ ### Added
22
+
23
+ - **`connecta.ui(html)` inside `execute_code`.** One non-empty HTML string, at
24
+ most one payload per run, delivered on success only in the tool result's
25
+ `_meta["connecta/ui"]` with `ui: true` on the JSON envelope. A failed program
26
+ discards it visibly (`uiDiscarded: true`), alongside `emittedDiscarded` when
27
+ one failure loses both. The payload spends the existing
28
+ `execute.maxEmittedBytes` aggregate, no block count, and no host calls.
29
+ - **The MCP Apps shell.** A static connecta-authored HTML5 template at
30
+ `ui://connecta/program-ui/v1` (`text/html;profile=mcp-app`), served by a
31
+ `resources/read` handler that answers exactly that URI; `resources/list` is
32
+ served and returns an empty list. `execute_code` declares it through
33
+ `_meta.ui.resourceUri` with `_meta.ui.visibility: ["model"]`, and the server
34
+ declares the `io.modelcontextprotocol/ui` extension — the one extension
35
+ connecta advertises, without which no host renders anything.
36
+ - **`diagnostics.ui`.** With `diagnostics: true`, the accepted payload's
37
+ serialized byte size, distinct from the `emitted` aggregate.
38
+
39
+ ### Fixed
40
+
41
+ - **Visible output discarded during QuickJS shutdown.** Closing the executor
42
+ after a program starts now reports accepted blocks and UI as discarded on
43
+ both error paths; admission failures before execution remain unchanged. (#279)
44
+
5
45
  ## 0.11.0 — 2026-07-31
6
46
 
7
47
  **This is a breaking deployment release.** Connecta no longer carries the old
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The one MCP Apps template connecta serves (`U5`, `U6`).
3
+ *
4
+ * A build-time string constant, not a file read at startup: the core is
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.
10
+ *
11
+ * The address carries a version segment because hosts are permitted to
12
+ * prefetch and cache templates by URI: change these bytes, bump `v1`.
13
+ */
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";
16
+ /** The mimeType the Apps spec requires of an HTML template. */
17
+ export declare const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
18
+ /**
19
+ * The result `_meta` key carrying the payload (`U3`). A plain single-label
20
+ * prefix rather than the reverse-DNS form MCP's SHOULD prefers: connecta has
21
+ * no domain to reverse, and fabricating one to satisfy a SHOULD is a worse
22
+ * answer than the shape the key format's MUST already permits.
23
+ */
24
+ export declare const PROGRAM_UI_META_KEY = "connecta/ui";
25
+ /** The one extension identifier connecta advertises (`U11`). */
26
+ export declare const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
27
+ /**
28
+ * The shell document. Dependency-free and deliberately small: it speaks the
29
+ * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
30
+ * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
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.
36
+ */
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";
38
+ //# sourceMappingURL=apps-shell.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The one MCP Apps template connecta serves (`U5`, `U6`).
3
+ *
4
+ * A build-time string constant, not a file read at startup: the core is
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.
10
+ *
11
+ * The address carries a version segment because hosts are permitted to
12
+ * prefetch and cache templates by URI: change these bytes, bump `v1`.
13
+ */
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";
16
+ /** The mimeType the Apps spec requires of an HTML template. */
17
+ export const PROGRAM_UI_MIME_TYPE = "text/html;profile=mcp-app";
18
+ /**
19
+ * The result `_meta` key carrying the payload (`U3`). A plain single-label
20
+ * prefix rather than the reverse-DNS form MCP's SHOULD prefers: connecta has
21
+ * no domain to reverse, and fabricating one to satisfy a SHOULD is a worse
22
+ * answer than the shape the key format's MUST already permits.
23
+ */
24
+ export const PROGRAM_UI_META_KEY = "connecta/ui";
25
+ /** The one extension identifier connecta advertises (`U11`). */
26
+ export const MCP_APPS_EXTENSION = "io.modelcontextprotocol/ui";
27
+ /**
28
+ * The shell document. Dependency-free and deliberately small: it speaks the
29
+ * Apps postMessage dialect (`ui/initialize`, `ui/notifications/initialized`,
30
+ * `ui/notifications/tool-result`, `ui/notifications/size-changed`,
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.
36
+ */
37
+ export const PROGRAM_UI_SHELL_HTML = `<!doctype html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="utf-8" />
41
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
42
+ <title>connecta program view</title>
43
+ <style>
44
+ html,
45
+ body {
46
+ margin: 0;
47
+ padding: 0;
48
+ background: transparent;
49
+ }
50
+ #program-view {
51
+ display: block;
52
+ width: 100%;
53
+ min-height: 480px;
54
+ border: 0;
55
+ }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <iframe
60
+ id="program-view"
61
+ title="Program-rendered view"
62
+ sandbox="allow-scripts"
63
+ srcdoc=""
64
+ ></iframe>
65
+ <script>
66
+ (function () {
67
+ "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.
74
+ var host = window.parent;
75
+ var view = document.getElementById("program-view");
76
+ var initializeId = "connecta-ui-initialize";
77
+ var lastWidth = 0;
78
+ var lastHeight = 0;
79
+
80
+ function send(message) {
81
+ if (!host || host === window) return;
82
+ host.postMessage(message, "*");
83
+ }
84
+
85
+ function notify(method, params) {
86
+ send({ jsonrpc: "2.0", method: method, params: params });
87
+ }
88
+
89
+ // 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
92
+ // what it reports here is its own box: the min-height above, unless
93
+ // the host has given it more. Taller content scrolls inside the inner
94
+ // frame rather than growing the view. Raising the min-height is the
95
+ // only lever; a content-height signal would cost the isolation.
96
+ function reportSize() {
97
+ var width = Math.ceil(document.documentElement.clientWidth);
98
+ var height = Math.ceil(document.documentElement.scrollHeight);
99
+ if (width === lastWidth && height === lastHeight) return;
100
+ lastWidth = width;
101
+ lastHeight = height;
102
+ notify("ui/notifications/size-changed", {
103
+ width: width,
104
+ height: height
105
+ });
106
+ }
107
+
108
+ function payloadHtml(result) {
109
+ if (!result || typeof result !== "object") return null;
110
+ var meta = result._meta;
111
+ 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;
116
+ }
117
+
118
+ 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;
125
+ reportSize();
126
+ }
127
+
128
+ window.addEventListener("message", function (event) {
129
+ if (event.source !== host) return;
130
+ var message = event.data;
131
+ if (!message || message.jsonrpc !== "2.0") return;
132
+ if (message.method === "ui/notifications/tool-result") {
133
+ render(message.params);
134
+ return;
135
+ }
136
+ if (message.method === "ui/resource-teardown") {
137
+ // A host->view request, not a notification: the host waits for
138
+ // this reply before it tears the view down. There is nothing to
139
+ // release, so answer immediately rather than make it time out.
140
+ if (message.id !== undefined && message.id !== null) {
141
+ send({ jsonrpc: "2.0", id: message.id, result: {} });
142
+ }
143
+ return;
144
+ }
145
+ // Only a completed handshake earns "initialized". A JSON-RPC error
146
+ // response carries the same id, and announcing initialization on one
147
+ // would assert a handshake that never happened.
148
+ if (message.id === initializeId && message.result !== undefined) {
149
+ notify("ui/notifications/initialized", {});
150
+ }
151
+ });
152
+
153
+ window.addEventListener("resize", reportSize);
154
+ view.addEventListener("load", reportSize);
155
+
156
+ // Every field here is required by the Apps initialize schema, and a
157
+ // conforming host rejects the request outright when one is missing —
158
+ // which would strand the shell before any tool result arrives.
159
+ send({
160
+ jsonrpc: "2.0",
161
+ id: initializeId,
162
+ method: "ui/initialize",
163
+ params: {
164
+ appInfo: { name: "connecta program view", version: "1" },
165
+ appCapabilities: {},
166
+ protocolVersion: "2026-01-26"
167
+ }
168
+ });
169
+ reportSize();
170
+ })();
171
+ </script>
172
+ </body>
173
+ </html>
174
+ `;
175
+ //# sourceMappingURL=apps-shell.js.map
@@ -0,0 +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"}
package/dist/execute.d.ts CHANGED
@@ -32,9 +32,16 @@ declare class ExecuteDiagnostics {
32
32
  setupMs: number;
33
33
  executorWallMs: number;
34
34
  private emitted?;
35
+ private ui?;
35
36
  /** Numbers only, per R8 — and only once something was emitted, so a
36
37
  * non-emitting run's diagnostics stay byte-for-byte what they were. */
37
38
  recordEmitted(count: number, bytes: number): void;
39
+ /**
40
+ * U9: the UI payload gets its own aggregate — one number, the payload's
41
+ * serialized size. Folding it into `emitted` would desync that aggregate's
42
+ * pair, which reports the bytes a specific block count cost.
43
+ */
44
+ recordUi(bytes: number): void;
38
45
  private stats;
39
46
  recordCatalog(operation: "search" | "describe", durationMs: number, ok: boolean, result?: unknown): void;
40
47
  recordCall(operation: "call" | "batch", outcome: {
@@ -61,6 +68,7 @@ declare class ExecuteDiagnostics {
61
68
  count: number;
62
69
  bytes: number;
63
70
  };
71
+ ui?: number;
64
72
  };
65
73
  }
66
74
  /**
@@ -81,20 +89,43 @@ export type EmittedBlock = {
81
89
  mimeType: string;
82
90
  };
83
91
  /**
84
- * Request-local collection for `connecta.emit`. Budgets fail loudly at the
85
- * crossing call — the block is not partially accepted and prior blocks are
86
- * unaffected — so a program learns it is over budget while it can still
87
- * choose differently (M5). Accepted blocks never ride `ExecuteResult`; the
88
- * handler that owns this collector appends them to the final tool result.
92
+ * Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
93
+ * loudly at the crossing call — nothing is partially accepted and prior blocks
94
+ * are unaffected — so a program learns it is over budget while it can still
95
+ * choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
96
+ * handler that owns this collector delivers it on the final tool result.
97
+ *
98
+ * The two channels share the byte aggregate and nothing else: a UI payload is
99
+ * not a block, so it spends no block count, and at most one is ever accepted.
89
100
  */
90
101
  export declare class EmitCollector {
91
102
  private readonly maxBytes;
92
103
  private readonly maxBlocks;
93
104
  private readonly diagnostics?;
94
105
  readonly blocks: EmittedBlock[];
106
+ /** The shared transport aggregate: emitted blocks plus the UI payload. */
95
107
  bytes: number;
108
+ /** The one accepted UI payload (U2), delivered in result `_meta` on success. */
109
+ ui?: {
110
+ html: string;
111
+ };
112
+ /** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
113
+ private blockBytes;
96
114
  constructor(maxBytes: number, maxBlocks: number, diagnostics?: ExecuteDiagnostics | undefined);
97
115
  accept(raw: unknown): void;
116
+ /**
117
+ * U2 and U4: one payload per run, measured as the serialized bytes of
118
+ * `{ html }` against the same aggregate emit spends. A second call throws
119
+ * naming the constraint rather than replacing the first — one tool result
120
+ * renders one view, and last-wins would silently discard a payload the
121
+ * program deliberately supplied.
122
+ *
123
+ * Multiplicity is checked before shape, so the second call is told what it
124
+ * actually broke. A program whose second payload is also malformed has one
125
+ * problem worth naming — that there is a second payload at all — and a
126
+ * complaint about its type would send the author to fix the wrong thing.
127
+ */
128
+ acceptUi(raw: unknown): void;
98
129
  }
99
130
  /** Convert a connector/tool name into a valid JS identifier. */
100
131
  export declare function sanitizeIdentifier(name: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../src/execute.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../src/execute.ts"],"names":[],"mappings":"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"}
package/dist/execute.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { z } from "zod";
2
+ import { PROGRAM_UI_META_KEY, PROGRAM_UI_RESOURCE_URI, } from "./apps-shell.js";
2
3
  import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchResult, } from "./catalog-service.js";
3
4
  import { errorResult, jsonResult } from "./meta-tools.js";
4
5
  import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
5
- import { ExecutorAdmissionError, isAdmittingExecutor, } from "./executor-admission.js";
6
+ import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
6
7
  import { classifyCallError } from "./errors.js";
7
8
  import { InvocationFailure, InvocationService, } from "./invocation.js";
8
9
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
@@ -26,12 +27,21 @@ class ExecuteDiagnostics {
26
27
  setupMs = 0;
27
28
  executorWallMs = 0;
28
29
  emitted;
30
+ ui;
29
31
  /** Numbers only, per R8 — and only once something was emitted, so a
30
32
  * non-emitting run's diagnostics stay byte-for-byte what they were. */
31
33
  recordEmitted(count, bytes) {
32
34
  if (count > 0)
33
35
  this.emitted = { count, bytes };
34
36
  }
37
+ /**
38
+ * U9: the UI payload gets its own aggregate — one number, the payload's
39
+ * serialized size. Folding it into `emitted` would desync that aggregate's
40
+ * pair, which reports the bytes a specific block count cost.
41
+ */
42
+ recordUi(bytes) {
43
+ this.ui = bytes;
44
+ }
35
45
  stats(operation) {
36
46
  let stats = this.operations.get(operation);
37
47
  if (!stats) {
@@ -100,6 +110,7 @@ class ExecuteDiagnostics {
100
110
  },
101
111
  operations,
102
112
  ...(this.emitted ? { emitted: this.emitted } : {}),
113
+ ...(this.ui !== undefined ? { ui: this.ui } : {}),
103
114
  };
104
115
  }
105
116
  }
@@ -134,19 +145,52 @@ function requireEmittedBlock(raw) {
134
145
  }
135
146
  return raw;
136
147
  }
148
+ const UI_SHAPE_HINT = "connecta.ui accepts exactly one argument: a non-empty string of HTML";
149
+ /** What the argument was, named the way the emit validator names a bad field. */
150
+ function describeUiArgument(raw) {
151
+ if (raw === null)
152
+ return "null";
153
+ if (raw === undefined)
154
+ return "undefined";
155
+ if (Array.isArray(raw))
156
+ return "an array";
157
+ if (typeof raw === "string")
158
+ return "an empty string";
159
+ const kind = typeof raw;
160
+ return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
161
+ }
162
+ /**
163
+ * Strict U1 validation. There is no options parameter and no sugar form, for
164
+ * M1's reason: sugar is how a one-shape contract grows hair. An options bag or
165
+ * an MCP block object is just a non-string, and fails as one.
166
+ */
167
+ function requireUiHtml(raw) {
168
+ if (typeof raw !== "string" || raw.length === 0) {
169
+ throw new Error(`${UI_SHAPE_HINT}; got ${describeUiArgument(raw)}`);
170
+ }
171
+ return raw;
172
+ }
137
173
  /**
138
- * Request-local collection for `connecta.emit`. Budgets fail loudly at the
139
- * crossing call — the block is not partially accepted and prior blocks are
140
- * unaffected — so a program learns it is over budget while it can still
141
- * choose differently (M5). Accepted blocks never ride `ExecuteResult`; the
142
- * handler that owns this collector appends them to the final tool result.
174
+ * Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
175
+ * loudly at the crossing call — nothing is partially accepted and prior blocks
176
+ * are unaffected — so a program learns it is over budget while it can still
177
+ * choose differently (M5, U4). Accepted output never rides `ExecuteResult`; the
178
+ * handler that owns this collector delivers it on the final tool result.
179
+ *
180
+ * The two channels share the byte aggregate and nothing else: a UI payload is
181
+ * not a block, so it spends no block count, and at most one is ever accepted.
143
182
  */
144
183
  export class EmitCollector {
145
184
  maxBytes;
146
185
  maxBlocks;
147
186
  diagnostics;
148
187
  blocks = [];
188
+ /** The shared transport aggregate: emitted blocks plus the UI payload. */
149
189
  bytes = 0;
190
+ /** The one accepted UI payload (U2), delivered in result `_meta` on success. */
191
+ ui;
192
+ /** What the blocks alone cost, so the `emitted` aggregate stays a true pair. */
193
+ blockBytes = 0;
150
194
  constructor(maxBytes, maxBlocks, diagnostics) {
151
195
  this.maxBytes = maxBytes;
152
196
  this.maxBlocks = maxBlocks;
@@ -163,7 +207,33 @@ export class EmitCollector {
163
207
  }
164
208
  this.blocks.push(block);
165
209
  this.bytes += size;
166
- this.diagnostics?.recordEmitted(this.blocks.length, this.bytes);
210
+ this.blockBytes += size;
211
+ this.diagnostics?.recordEmitted(this.blocks.length, this.blockBytes);
212
+ }
213
+ /**
214
+ * U2 and U4: one payload per run, measured as the serialized bytes of
215
+ * `{ html }` against the same aggregate emit spends. A second call throws
216
+ * naming the constraint rather than replacing the first — one tool result
217
+ * renders one view, and last-wins would silently discard a payload the
218
+ * program deliberately supplied.
219
+ *
220
+ * Multiplicity is checked before shape, so the second call is told what it
221
+ * actually broke. A program whose second payload is also malformed has one
222
+ * problem worth naming — that there is a second payload at all — and a
223
+ * complaint about its type would send the author to fix the wrong thing.
224
+ */
225
+ acceptUi(raw) {
226
+ if (this.ui) {
227
+ throw new Error("connecta.ui accepts at most one payload per run: a view was already accepted and stands");
228
+ }
229
+ const payload = { html: requireUiHtml(raw) };
230
+ const size = diagnosticsEncoder.encode(JSON.stringify(payload)).byteLength;
231
+ if (this.bytes + size > this.maxBytes) {
232
+ throw new Error(`connecta.ui byte budget exceeded: payload is ${size} serialized bytes with ${this.maxBytes - this.bytes} of ${this.maxBytes} remaining`);
233
+ }
234
+ this.ui = payload;
235
+ this.bytes += size;
236
+ this.diagnostics?.recordUi(size);
167
237
  }
168
238
  }
169
239
  /** A configured emit budget must be a finite number >= 1; anything else falls back. */
@@ -393,6 +463,16 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
393
463
  }
394
464
  limits.emitCollector.accept(block);
395
465
  },
466
+ // The rendered-output channel rides the same bridge for the same
467
+ // reason (U7): one more provider fn, no change to ExecuteResult or
468
+ // the Executor contract. Delivery is the handler's job, not the
469
+ // guest's — nothing here becomes addressable.
470
+ ui: async (html) => {
471
+ if (!limits.emitCollector) {
472
+ throw new Error("connecta.ui is unavailable: no emission collector was configured for this execution");
473
+ }
474
+ limits.emitCollector.acceptUi(html);
475
+ },
396
476
  batch: async (calls) => {
397
477
  const started = Date.now();
398
478
  const callCount = Array.isArray(calls) ? calls.length : 0;
@@ -568,6 +648,9 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
568
648
  ? { retryAfterMs: err.retryAfterMs }
569
649
  : {}),
570
650
  },
651
+ ...(err instanceof ExecutorExecutionError
652
+ ? discardedEmits(emitted)
653
+ : {}),
571
654
  ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
572
655
  });
573
656
  result.isError = true;
@@ -684,9 +767,19 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
684
767
  const response = jsonResult({
685
768
  result,
686
769
  ...(emitted.blocks.length > 0 ? { emitted: emitted.blocks.length } : {}),
770
+ // U3: the model learns a view rendered without seeing its bytes. Spread
771
+ // conditionally so a program that never called connecta.ui produces
772
+ // today's byte-for-byte response.
773
+ ...(emitted.ui ? { ui: true } : {}),
687
774
  ...(logs ? { logs } : {}),
688
775
  ...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
689
776
  });
777
+ if (emitted.ui) {
778
+ // _meta is where the Apps spec's best practices put data "not intended
779
+ // for model context", and how shipped hosts behave. The shell reads
780
+ // exactly this key out of the tool result the host delivers to it.
781
+ response._meta = { [PROGRAM_UI_META_KEY]: { html: emitted.ui.html } };
782
+ }
690
783
  if (emitted.blocks.length > 0) {
691
784
  // Emitted image/audio blocks are valid MCP content that ToolResult's
692
785
  // text-only typing does not model — the same acknowledged gap
@@ -696,17 +789,27 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
696
789
  return response;
697
790
  };
698
791
  }
699
- /** M4: a failed program delivers no blocks, but the discard is visible. */
792
+ /**
793
+ * M4 and U3: a failed program delivers no blocks and no view, but each
794
+ * discard is visible — and one failure can discard both.
795
+ */
700
796
  function discardedEmits(emitted) {
701
- return emitted.blocks.length > 0
702
- ? { emittedDiscarded: emitted.blocks.length }
703
- : {};
797
+ return {
798
+ ...(emitted.blocks.length > 0
799
+ ? { emittedDiscarded: emitted.blocks.length }
800
+ : {}),
801
+ ...(emitted.ui ? { uiDiscarded: true } : {}),
802
+ };
704
803
  }
705
804
  /** The same visibility for the plain-text error paths. */
706
805
  function discardedEmitsText(emitted) {
707
- return emitted.blocks.length > 0
708
- ? `\n\nemittedDiscarded: ${emitted.blocks.length}`
709
- : "";
806
+ const lines = [
807
+ ...(emitted.blocks.length > 0
808
+ ? [`emittedDiscarded: ${emitted.blocks.length}`]
809
+ : []),
810
+ ...(emitted.ui ? ["uiDiscarded: true"] : []),
811
+ ];
812
+ return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
710
813
  }
711
814
  const executeDescription = (emitBudgets) => `The primary surface. Use for discovery beyond one lookup, two or more calls, dependent steps, loops, joins, branching, or reducing large results before they reach the model — connecta.search and connecta.describe browse and expand catalogs in the run, and connecta.batch handles independent calls. The exception is a single call at an address already in hand: search_tools then one call_tool is cheaper than a program. Only tools explicitly annotated readOnlyHint: true are available. Each run is limited to ${EXECUTE_MAX_HOST_CALLS} host calls; connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS}; each host call has a ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second deadline.
712
815
 
@@ -715,6 +818,7 @@ Write an async arrow function. It runs with NO network, filesystem, timers, or i
715
818
  - connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
716
819
  - connecta.search(args), connecta.describe({ address: "<connectorId>.<toolName>" }), and connecta.describe({ addresses: [...] }) — load and inspect request-local catalogs on demand. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute; the filter changes results, not authority. Matches carrying schemas also list inputKeys, requiredInputKeys, and outputKeys — the same names the schema shows, ready to check against before building args. They are absent when a schema is not a plain object shape, so read the schema itself rather than assuming a missing list means no fields.
717
820
  - connecta.emit(block) — deliver rich MCP content alongside the JSON return: exactly { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }, no other fields. Blocks are appended to the result on success only, spend no host calls, and are budgeted per run (${emitBudgets.maxBlocks} blocks, ${emitBudgets.maxBytes} serialized bytes); an over-budget or invalid emit throws catchably and accepts nothing.
821
+ - connecta.ui(html) — hand the client one rendered view of this run: exactly one argument, a non-empty HTML string, no options and no block object. At most one per run, delivered on success only, out of model context (the envelope just reports ui: true), and spending no host calls. It draws on the same ${emitBudgets.maxBytes}-byte budget as connecta.emit; a second, over-budget, or invalid call throws catchably and accepts nothing. The view is display-only — no network, no tool calls, no links.
718
822
  - console.log(...) — captured and returned alongside the result.
719
823
 
720
824
  Tool calls return plain values (MCP text content is JSON-parsed when possible) and throw on downstream errors — use try/catch to handle them. A thrown error carries only a message; connecta.batch reports each call as { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }, so use it when the program must tell a policy refusal from a transient failure. Never retry a failure whose retryable is false, and never retry a rate_limited one immediately — the sandbox has no timers. Return a JSON-serializable value; large results are truncated, so reduce data in code instead of returning raw payloads.
@@ -757,6 +861,18 @@ export function registerExecuteTool(server, registry, ctx) {
757
861
  destructiveHint: false,
758
862
  openWorldHint: true,
759
863
  },
864
+ // U5 and U10: declared unconditionally. A host without the Apps
865
+ // extension ignores unknown _meta and sees the ordinary envelope, which
866
+ // is the text fallback the spec mandates — and a stateless aggregator
867
+ // has nowhere dependable to hold a negotiation check anyway. The
868
+ // explicit visibility keeps hosts from being told the view may call
869
+ // execute_code; the default ["model","app"] would say exactly that.
870
+ _meta: {
871
+ ui: {
872
+ resourceUri: PROGRAM_UI_RESOURCE_URI,
873
+ visibility: ["model"],
874
+ },
875
+ },
760
876
  }, async (args, extra) => {
761
877
  const controller = new AbortController();
762
878
  const signals = [extra.mcpReq.signal, ctx.requestSignal].filter((signal) => signal !== undefined);