@zackbart/connecta 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +387 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/artifacts/connector.d.ts +55 -0
  9. package/dist/artifacts/connector.js +724 -0
  10. package/dist/artifacts/document.js +80 -0
  11. package/dist/artifacts/guide.js +140 -0
  12. package/dist/artifacts/html-scan.js +329 -0
  13. package/dist/artifacts/json.js +68 -0
  14. package/dist/artifacts/kv-store.d.ts +20 -0
  15. package/dist/artifacts/kv-store.js +140 -0
  16. package/dist/artifacts/markdown.js +478 -0
  17. package/dist/artifacts/module.d.ts +37 -0
  18. package/dist/artifacts/module.js +60 -0
  19. package/dist/artifacts/operations.d.ts +195 -0
  20. package/dist/artifacts/operations.js +944 -0
  21. package/dist/artifacts/refresh.d.ts +53 -0
  22. package/dist/artifacts/refresh.js +235 -0
  23. package/dist/artifacts/routes.js +217 -0
  24. package/dist/artifacts/types.d.ts +214 -0
  25. package/dist/artifacts/types.js +22 -0
  26. package/dist/artifacts/validate.d.ts +62 -0
  27. package/dist/artifacts/validate.js +653 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/auth/downstream-oauth.js +397 -48
  31. package/dist/auth/oauth-start-reset.js +20 -0
  32. package/dist/branding.d.ts +87 -0
  33. package/dist/branding.js +170 -1
  34. package/dist/catalog-service.js +39 -29
  35. package/dist/catalog.js +42 -8
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connectors/remote-mcp.js +48 -9
  41. package/dist/errors.d.ts +16 -1
  42. package/dist/errors.js +28 -0
  43. package/dist/execute.js +431 -75
  44. package/dist/executor-admission.d.ts +11 -1
  45. package/dist/executor-admission.js +79 -14
  46. package/dist/executors/quickjs.js +4 -2
  47. package/dist/index.d.ts +62 -6
  48. package/dist/index.js +230 -6
  49. package/dist/invocation.js +56 -10
  50. package/dist/meta-tools.js +7 -2
  51. package/dist/module-contracts.d.ts +21 -1
  52. package/dist/oauth-callback-outcome.js +43 -18
  53. package/dist/operator-ui/generated.js +2 -2
  54. package/dist/operator-ui/model.d.ts +62 -5
  55. package/dist/operator-ui/view.js +372 -28
  56. package/dist/page-styles.js +7 -0
  57. package/dist/program-source.js +116 -0
  58. package/dist/registry.d.ts +42 -6
  59. package/dist/registry.js +164 -60
  60. package/dist/resumable.js +1291 -0
  61. package/dist/routes/activity.js +6 -69
  62. package/dist/routes/actor-labels.js +114 -0
  63. package/dist/routes/credentials.js +27 -5
  64. package/dist/routes/mcp.js +120 -59
  65. package/dist/routes/oauth-management.js +87 -20
  66. package/dist/routes/oauth.js +54 -87
  67. package/dist/routes/operator.js +1 -0
  68. package/dist/routes/shared.d.ts +23 -1
  69. package/dist/routes/shared.js +20 -2
  70. package/dist/routes/ui-data.js +2 -1
  71. package/dist/routes/ui.js +19 -4
  72. package/dist/run-journal.js +347 -0
  73. package/dist/runtime/shared-read.js +106 -0
  74. package/dist/server.js +30 -2
  75. package/dist/skills.js +30 -11
  76. package/dist/timeout.js +26 -0
  77. package/dist/tool-safety.d.ts +35 -0
  78. package/dist/tool-safety.js +31 -0
  79. package/dist/types.d.ts +23 -0
  80. package/dist/ui.d.ts +17 -5
  81. package/dist/ui.js +51 -68
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/documentation/architecture.md +214 -27
  85. package/documentation/auth.md +186 -2
  86. package/documentation/code-mode.md +236 -63
  87. package/documentation/meta-tools.md +55 -29
  88. package/ethos.md +12 -10
  89. package/examples/worker/AGENTS.md +6 -0
  90. package/examples/worker/README.md +174 -10
  91. package/examples/worker/src/d1-activity-row.ts +14 -2
  92. package/examples/worker/src/d1-activity.ts +4 -2
  93. package/examples/worker/src/index.ts +36 -1
  94. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  95. package/examples/worker/wrangler.jsonc +5 -1
  96. package/package.json +6 -2
  97. package/templates/node/AGENTS.md +9 -2
  98. package/templates/node/README.md +31 -3
  99. package/templates/node/package.json +1 -1
  100. package/templates/node/src/index.ts +27 -0
@@ -0,0 +1,35 @@
1
+ import type { Connector, ToolDef } from "./types.js";
2
+ /**
3
+ * The one fail-closed classification shared by discovery and invocation.
4
+ *
5
+ * A tool is read-only only when it says so without also saying it is
6
+ * destructive. Missing, false, and contradictory annotations all require the
7
+ * approval-visible call path.
8
+ */
9
+ export declare function isExplicitlyReadOnly(definition: ToolDef): boolean;
10
+ /**
11
+ * A deployment's approval exemptions, resolved once at construction from
12
+ * `execute.approval` (#566). Config is the only source: nothing an agent, the
13
+ * operator UI, or a downstream catalog says can add an entry.
14
+ */
15
+ export interface ApprovalPolicy {
16
+ /** Connector id → the default for that connector's tools. */
17
+ readonly connectors: ReadonlyMap<string, "never" | "ask">;
18
+ /** Canonical `connector.tool` address → that tool's own setting. */
19
+ readonly tools: ReadonlyMap<string, "never" | "ask">;
20
+ }
21
+ export declare const NO_EXEMPTIONS: ApprovalPolicy;
22
+ /**
23
+ * Whether a program may call this tool without pausing for approval.
24
+ *
25
+ * Only a tool that is *not* explicitly read-only can be exempt — a read-only
26
+ * tool needs no exemption, and reporting one would blur the two classes an
27
+ * exemption must never merge. The most specific setting wins: the tool's
28
+ * address, then its connector's entry, then the connector's own default,
29
+ * then `"ask"`. Annotations play no part beyond that first test: a downstream
30
+ * that could annotate its way out of approval would be granting itself the
31
+ * capability this exists to keep in config. Exempt never means read-only:
32
+ * discovery keeps it in the approval-required class, and `call_tool` still
33
+ * refuses it.
34
+ */
35
+ export declare function isApprovalExempt(policy: ApprovalPolicy, connector: Pick<Connector, "id" | "approval">, toolName: string, definition: ToolDef): boolean;
@@ -9,3 +9,34 @@ export function isExplicitlyReadOnly(definition) {
9
9
  return (definition.annotations?.readOnlyHint === true &&
10
10
  definition.annotations?.destructiveHint !== true);
11
11
  }
12
+ export const NO_EXEMPTIONS = {
13
+ connectors: new Map(),
14
+ tools: new Map(),
15
+ };
16
+ /**
17
+ * Whether a program may call this tool without pausing for approval.
18
+ *
19
+ * Only a tool that is *not* explicitly read-only can be exempt — a read-only
20
+ * tool needs no exemption, and reporting one would blur the two classes an
21
+ * exemption must never merge. The most specific setting wins: the tool's
22
+ * address, then its connector's entry, then the connector's own default,
23
+ * then `"ask"`. Annotations play no part beyond that first test: a downstream
24
+ * that could annotate its way out of approval would be granting itself the
25
+ * capability this exists to keep in config. Exempt never means read-only:
26
+ * discovery keeps it in the approval-required class, and `call_tool` still
27
+ * refuses it.
28
+ */
29
+ export function isApprovalExempt(policy, connector, toolName, definition) {
30
+ if (isExplicitlyReadOnly(definition))
31
+ return false;
32
+ // This tool starts another program. Letting a program call it while holding
33
+ // the only executor permit would deadlock; manual refresh is a top-level
34
+ // action, even though other artifact writes are exempt inside programs.
35
+ if (connector.id === "artifacts" && toolName === "run_refresh")
36
+ return false;
37
+ const setting = policy.tools.get(`${connector.id}.${toolName}`) ??
38
+ policy.connectors.get(connector.id) ??
39
+ connector.approval ??
40
+ "ask";
41
+ return setting === "never";
42
+ }
package/dist/types.d.ts CHANGED
@@ -149,6 +149,11 @@ export interface ConnectorCredentialConfig {
149
149
  }
150
150
  export interface CredentialTestResult {
151
151
  ok: boolean;
152
+ /**
153
+ * Detail for the deployment's log. The operator page shows fixed copy for
154
+ * `ok` and never this text, since a downstream's reply quoted here can
155
+ * quote the credential it rejected.
156
+ */
152
157
  message?: string;
153
158
  }
154
159
  export interface ConnectorContext {
@@ -212,6 +217,11 @@ export interface ConnectorStatus {
212
217
  state: ConnectorStatusState;
213
218
  /** When state === "auth_required", the URL the operator should open. */
214
219
  authorizationUrl?: string;
220
+ /**
221
+ * Set by a non-forced `startAuth` that handed back a still-recent pending
222
+ * authorization URL instead of starting a new flow. Absent otherwise.
223
+ */
224
+ authorizationReused?: boolean;
215
225
  message?: string;
216
226
  /**
217
227
  * Drift observed the last time this connector served a catalog refresh *in
@@ -240,6 +250,17 @@ export interface Connector {
240
250
  title?: string;
241
251
  /** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
242
252
  kind?: "mcp" | "api";
253
+ /**
254
+ * This connector's own default for its tools that are not explicitly
255
+ * read-only, inside `execute_code`: `"never"` lets a program call them
256
+ * without pausing for approval. Reserved for connectors connecta ships
257
+ * whose writes are cheap to undo (the planned artifacts connector, whose
258
+ * every write is a new immutable version); a deployment's own connectors
259
+ * leave it unset and exempt through `execute.approval`, which overrides
260
+ * this either way. Never read from a downstream catalog — a server cannot
261
+ * exempt its own tools — and never makes a tool read-only anywhere else.
262
+ */
263
+ approval?: "never";
243
264
  description?: string;
244
265
  /**
245
266
  * Max inline result size (bytes) for this connector's tools before
@@ -394,6 +415,8 @@ export interface AdmissionSnapshot {
394
415
  maxQueueSize: number;
395
416
  queueTimeoutMs: number;
396
417
  retryAfterMs: number;
418
+ /** Hard admitted-request lifetime, present only on the /mcp pool. */
419
+ maxDurationMs?: number;
397
420
  active: number;
398
421
  queued: number;
399
422
  closed: boolean;
package/dist/ui.d.ts CHANGED
@@ -2,18 +2,21 @@ import type { OperatorSurface } from "./module-contracts.js";
2
2
  import type { CredentialVault } from "./credential-contract.js";
3
3
  import { type DeferredWork } from "./connector-scope.js";
4
4
  import { type CredentialManagementCapability, type UiData, type UiProblem, type UiToolSafety } from "./operator-ui/model.js";
5
+ import { type ApprovalPolicy } from "./tool-safety.js";
5
6
  import type { RegistryView } from "./registry.js";
6
7
  import type { ConnectaBranding, Connector, ConnectorStatus, ToolDef, UiAuthConfig } from "./types.js";
7
8
  export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
8
9
  export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
9
- export type OperatorPage = "connections" | "activity";
10
+ export type OperatorPage = "connections" | "activity" | "artifacts" | "artifact";
10
11
  export declare function operatorPageForPath(path: string): OperatorPage | undefined;
11
12
  export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
12
13
  /**
13
- * The badge a tool earns, from the one predicate core enforces with. Computed
14
- * here so the page reports the rule rather than restating it.
14
+ * The badge a tool earns, from the predicates core enforces with. Computed
15
+ * here so the page reports the rule rather than restating it. `exempt` is the
16
+ * config exemption (#566), which `isApprovalExempt` already refuses to grant
17
+ * a read-only tool.
15
18
  */
16
- export declare function uiToolSafety(definition: ToolDef): UiToolSafety;
19
+ export declare function uiToolSafety(definition: ToolDef, exempt?: boolean): UiToolSafety;
17
20
  /**
18
21
  * The fix-prompt key for a connector that is not usable. Chosen from the
19
22
  * status state and the connector's declared auth shape — configuration, never
@@ -40,12 +43,21 @@ export declare function buildUiData(registry: RegistryView, baseUrl: string, ser
40
43
  mayManage?: (id: string) => boolean;
41
44
  timeoutMs?: number;
42
45
  signal?: AbortSignal;
46
+ approval?: ApprovalPolicy;
43
47
  }): Promise<UiData>;
44
48
  /**
45
49
  * Every operator page serves this same data-free shell. Connector, credential,
46
50
  * and activity data arrives only through the authenticated `/ui/*` APIs.
47
51
  */
48
- export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage): string;
52
+ export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage, options?: {
53
+ /** Where the Connections page lives, when this shell is on another origin. */
54
+ homeUrl?: string;
55
+ /**
56
+ * The origin that serves `/favicon.*`, when this shell is on another one:
57
+ * an artifact host answers those paths with 404. See `PageLayout.iconOrigin`.
58
+ */
59
+ iconOrigin?: string | undefined;
60
+ }): string;
49
61
  /** Mount the connection UI without enabling any storage or activity module. */
50
62
  export declare function operatorUi(options?: {
51
63
  branding?: ConnectaBranding;
package/dist/ui.js CHANGED
@@ -6,7 +6,7 @@ import { runEdge } from "./runtime/run.js";
6
6
  import { isExplicitlyReadOnly } from "./tool-safety.js";
7
7
  import { OPERATOR_UI_CSS, OPERATOR_UI_SCRIPT, } from "./operator-ui/generated.js";
8
8
  export { filterUiConnectors, } from "./operator-ui/model.js";
9
- import { resolveBranding, isSafeHttpsUrl, themeCss } from "./branding.js";
9
+ import { escapeHtml, isSafeHttpsUrl, notFoundResponse, renderPage, resolveBranding, } from "./branding.js";
10
10
  export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
11
11
  /**
12
12
  * A JS string literal safe to inline in a script element. Escaping `/` keeps
@@ -18,23 +18,39 @@ function stringForInlineScript(value) {
18
18
  const OPERATOR_PAGE_LABELS = {
19
19
  connections: "Connections",
20
20
  activity: "Activity",
21
+ artifacts: "Artifacts",
22
+ artifact: "Artifact",
21
23
  };
24
+ /** `/artifacts/<id>` and its snapshots, `/artifacts/<id>/v/<version>`. */
25
+ const ARTIFACT_PAGE = /^\/artifacts\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\/v\/\d{1,9})?$/;
22
26
  export function operatorPageForPath(path) {
23
27
  if (path === "/")
24
28
  return "connections";
25
29
  if (path === "/activity")
26
30
  return "activity";
31
+ if (path === "/artifacts")
32
+ return "artifacts";
33
+ if (ARTIFACT_PAGE.test(path))
34
+ return "artifact";
27
35
  return undefined;
28
36
  }
37
+ /** Everything artifact pages own: shells, their API, and the frame. */
38
+ function isArtifactPath(path) {
39
+ return path === "/artifacts" || path.startsWith("/artifacts/");
40
+ }
29
41
  export function operatorPageTitle(page, configuredTitle) {
30
42
  return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
31
43
  }
32
44
  /**
33
- * The badge a tool earns, from the one predicate core enforces with. Computed
34
- * here so the page reports the rule rather than restating it.
45
+ * The badge a tool earns, from the predicates core enforces with. Computed
46
+ * here so the page reports the rule rather than restating it. `exempt` is the
47
+ * config exemption (#566), which `isApprovalExempt` already refuses to grant
48
+ * a read-only tool.
35
49
  */
36
- export function uiToolSafety(definition) {
37
- return isExplicitlyReadOnly(definition) ? "runs_in_programs" : "needs_approval";
50
+ export function uiToolSafety(definition, exempt = false) {
51
+ if (isExplicitlyReadOnly(definition))
52
+ return "runs_in_programs";
53
+ return exempt ? "exempt" : "needs_approval";
38
54
  }
39
55
  /**
40
56
  * The fix-prompt key for a connector that is not usable. Chosen from the
@@ -82,13 +98,6 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
82
98
  ...detailOptions,
83
99
  }));
84
100
  }
85
- function escapeHtmlAttr(value) {
86
- return value
87
- .replaceAll("&", "&amp;")
88
- .replaceAll('"', "&quot;")
89
- .replaceAll("<", "&lt;")
90
- .replaceAll(">", "&gt;");
91
- }
92
101
  function jsonForInlineScript(value) {
93
102
  return JSON.stringify(value)
94
103
  .replaceAll("<", "\\u003c")
@@ -99,7 +108,7 @@ function jsonForInlineScript(value) {
99
108
  * Every operator page serves this same data-free shell. Connector, credential,
100
109
  * and activity data arrives only through the authenticated `/ui/*` APIs.
101
110
  */
102
- export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections") {
111
+ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections", options = {}) {
103
112
  const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
104
113
  // The Clerk loader's origin. A value that fails the gate is dropped rather
105
114
  // than escaped into the page: the loader tag is simply not emitted, the gate
@@ -129,81 +138,46 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
129
138
  }
130
139
  : (uiAuth ?? { kind: "bearer" });
131
140
  const brand = resolveBranding(branding);
132
- const title = operatorPageTitle(page, brand.pageTitle);
133
141
  // When an operator shell ships a nonce-based CSP, every script it emits must
134
142
  // carry that nonce to run; without a nonce the markup is unchanged.
135
143
  const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
136
- // Top-left corner. With an owner set it reads "<owner> <product>"; without
137
- // one the product label stands alone. Either half links out when the
138
- // matching URL is configured.
139
- const owner = brand.ownerName
140
- ? brand.ownerUrl
141
- ? `<a class="brand navlink" href="${escapeHtmlAttr(brand.ownerUrl)}">${escapeHtmlAttr(brand.ownerName)}</a>`
142
- : `<span class="brand">${escapeHtmlAttr(brand.ownerName)}</span>`
143
- : brand.productUrl
144
- ? `<a class="brand navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
145
- : `<span class="brand">${escapeHtmlAttr(brand.productName)}</span>`;
146
- const product = brand.ownerName
147
- ? brand.productUrl
148
- ? `<a class="product navlink" href="${escapeHtmlAttr(brand.productUrl)}">${escapeHtmlAttr(brand.productName)}</a>`
149
- : `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
150
- : "";
151
144
  const clerkScript = clerk && clerkScriptOrigin
152
- ? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
145
+ ? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtml(clerk.publishableKey)}" src="${escapeHtml(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
153
146
  : "";
154
- // A pinned scheme is an attribute, not a stylesheet edit: the dark palette
155
- // keys off `html[data-scheme]`, so "system" leaves the attribute off and the
156
- // media query decides.
157
- const schemeAttr = brand.theme.colorScheme === "system"
158
- ? ""
159
- : ` data-scheme="${brand.theme.colorScheme}"`;
160
- return `<!doctype html>
161
- <html lang="en"${schemeAttr}>
162
- <head>
163
- <meta charset="utf-8">
164
- <meta name="viewport" content="width=device-width, initial-scale=1">
165
- <meta name="theme-color" content="${escapeHtmlAttr(brand.themeColor)}">
166
- <meta name="description" content="${escapeHtmlAttr(brand.description)}">
167
- <link rel="icon" href="${escapeHtmlAttr(brand.faviconHref)}" type="image/svg+xml">
168
- <link rel="shortcut icon" href="/favicon.ico">
169
- <title>${escapeHtmlAttr(title)}</title>
170
- ${clerkScript}
171
- <style>${OPERATOR_UI_CSS}${themeCss(brand.theme)}</style>
172
- </head>
173
- <body>
174
- <a class="skip-link" href="#operatorContent">Skip to operator page</a>
175
- <header class="masthead shell">
176
- <div class="masthead-inner">
177
- <div class="mast-nav">
178
- ${owner}
179
- ${product}
180
- </div>
181
- <div id="operatorNav"></div>
182
- </div>
183
- </header>
184
-
147
+ // The shared layout owns the head, scheme, theme, and masthead; the shell
148
+ // adds its own bundle (which already imports the shared tokens), the nav
149
+ // mount, and the app.
150
+ return renderPage(branding, {
151
+ title: operatorPageTitle(page, brand.pageTitle),
152
+ uiMounted: true,
153
+ iconOrigin: options.iconOrigin,
154
+ styles: OPERATOR_UI_CSS,
155
+ head: clerkScript,
156
+ skipTo: { id: "operatorContent", label: "Skip to operator page" },
157
+ mastheadEnd: '<div id="operatorNav"></div>',
158
+ body: `
185
159
  <main id="operatorContent" class="page shell" tabindex="-1">
186
160
  <div class="lead">
187
161
  <h1>${OPERATOR_PAGE_LABELS[page]}</h1>
188
162
  <div class="lead-copy">
189
- <p>${escapeHtmlAttr(brand.description)}</p>
163
+ <p>${escapeHtml(brand.description)}</p>
190
164
  <noscript><p class="msg">The operator pages need JavaScript. Nothing else here
191
165
  does — agents reach this deployment through <span class="mono">/mcp</span>.</p></noscript>
192
166
  </div>
193
167
  </div>
194
168
  </main>
195
-
196
- <script${nonceAttr}>
169
+ `,
170
+ tail: `<script${nonceAttr}>
197
171
  const AUTH = ${jsonForInlineScript(auth)};
198
172
  const MCP_URL = ${jsonForInlineScript(mcpUrl)};
199
173
  const INITIAL_PAGE = ${jsonForInlineScript(page)};
174
+ const HOME_URL = ${jsonForInlineScript(options.homeUrl ?? "/")};
200
175
  const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
201
176
  const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
202
177
  const PRODUCT_DESCRIPTION = ${stringForInlineScript(brand.description)};
203
178
  const PRODUCT_OPERATOR_LABEL = ${stringForInlineScript(brand.productName + " operator")};
204
- ${OPERATOR_UI_SCRIPT}</script>
205
- </body>
206
- </html>`;
179
+ ${OPERATOR_UI_SCRIPT}</script>`,
180
+ });
207
181
  }
208
182
  function ownsOperatorPath(reserved, path) {
209
183
  if (path === "/activity")
@@ -226,8 +200,17 @@ export function operatorUi(options = {}) {
226
200
  // route here, and no activity module, runs for a path this surface does
227
201
  // not own. The Activity page is the one addition; the server reserves
228
202
  // it only when history is readable, and routeUi checks the same thing.
229
- if (!ownsOperatorPath(reservedPaths, context.path))
203
+ // Artifact pages are the other: they exist only beside the module.
204
+ const artifacts = context.opts.artifactsModule;
205
+ const artifactPath = Boolean(artifacts) && isArtifactPath(context.path);
206
+ if (!artifactPath && !ownsOperatorPath(reservedPaths, context.path))
230
207
  return null;
208
+ if (artifactPath && !operatorPageForPath(context.path)) {
209
+ // The pages' JSON API and the sandboxed frame; the module owns both,
210
+ // and this bundle imports none of it.
211
+ return (await artifacts?.handle(context)) ??
212
+ notFoundResponse(context.request, context.opts);
213
+ }
231
214
  const routes = [
232
215
  ...(context.opts.credentialVault ? [routeCredentials] : []),
233
216
  routeOAuthManagement,
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.25.0";
7
+ export declare const CONNECTA_VERSION = "0.26.1";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.25.0";
7
+ export const CONNECTA_VERSION = "0.26.1";