@zackbart/connecta 0.25.0 → 0.26.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 (93) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +271 -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 +76 -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 +449 -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 +194 -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 +652 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/catalog-service.js +39 -29
  31. package/dist/catalog.js +42 -8
  32. package/dist/connector-access.d.ts +18 -4
  33. package/dist/connector-access.js +65 -20
  34. package/dist/connector-caller.d.ts +8 -0
  35. package/dist/connector-caller.js +19 -0
  36. package/dist/connectors/remote-mcp.js +16 -2
  37. package/dist/errors.d.ts +16 -1
  38. package/dist/errors.js +28 -0
  39. package/dist/execute.js +427 -70
  40. package/dist/executor-admission.d.ts +11 -1
  41. package/dist/executor-admission.js +79 -14
  42. package/dist/executors/quickjs.js +4 -2
  43. package/dist/index.d.ts +62 -6
  44. package/dist/index.js +230 -6
  45. package/dist/invocation.js +56 -10
  46. package/dist/meta-tools.js +7 -2
  47. package/dist/module-contracts.d.ts +21 -1
  48. package/dist/operator-ui/generated.js +2 -2
  49. package/dist/operator-ui/model.d.ts +62 -5
  50. package/dist/operator-ui/view.js +94 -5
  51. package/dist/program-source.js +116 -0
  52. package/dist/registry.d.ts +42 -6
  53. package/dist/registry.js +148 -47
  54. package/dist/resumable.js +1291 -0
  55. package/dist/routes/activity.js +6 -69
  56. package/dist/routes/actor-labels.js +114 -0
  57. package/dist/routes/credentials.js +27 -5
  58. package/dist/routes/mcp.js +116 -59
  59. package/dist/routes/oauth-management.js +22 -9
  60. package/dist/routes/operator.js +1 -0
  61. package/dist/routes/shared.d.ts +23 -1
  62. package/dist/routes/shared.js +20 -2
  63. package/dist/routes/ui-data.js +2 -1
  64. package/dist/routes/ui.js +13 -4
  65. package/dist/run-journal.js +347 -0
  66. package/dist/runtime/shared-read.js +106 -0
  67. package/dist/server.js +26 -0
  68. package/dist/skills.js +27 -8
  69. package/dist/timeout.js +26 -0
  70. package/dist/tool-safety.d.ts +35 -0
  71. package/dist/tool-safety.js +31 -0
  72. package/dist/types.d.ts +18 -0
  73. package/dist/ui.d.ts +12 -5
  74. package/dist/ui.js +32 -6
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/documentation/architecture.md +196 -19
  78. package/documentation/auth.md +83 -2
  79. package/documentation/code-mode.md +221 -48
  80. package/documentation/meta-tools.md +44 -25
  81. package/ethos.md +12 -10
  82. package/examples/worker/AGENTS.md +6 -0
  83. package/examples/worker/README.md +174 -10
  84. package/examples/worker/src/d1-activity-row.ts +14 -2
  85. package/examples/worker/src/d1-activity.ts +4 -2
  86. package/examples/worker/src/index.ts +36 -1
  87. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  88. package/examples/worker/wrangler.jsonc +5 -1
  89. package/package.json +6 -2
  90. package/templates/node/AGENTS.md +9 -2
  91. package/templates/node/README.md +31 -3
  92. package/templates/node/package.json +1 -1
  93. package/templates/node/src/index.ts +27 -0
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { createMcpRoute, MCP_CORS_HEADERS } from "./routes/mcp.js";
5
5
  import { routeOAuthCallback, } from "./routes/oauth.js";
6
6
  import { runEdge } from "./runtime/run.js";
7
7
  import { withSecurityHeaders, } from "./routes/shared.js";
8
+ const isArtifactPath = (path) => path === "/artifacts" || path.startsWith("/artifacts/");
8
9
  /**
9
10
  * Build the Web-standard fetch handler.
10
11
  *
@@ -50,6 +51,10 @@ export function createFetchHandler(opts) {
50
51
  ...(opts.executorName !== undefined
51
52
  ? { executor: { name: opts.executorName } }
52
53
  : {}),
54
+ // Whether a program's write pauses for resume_execution or fails E4.
55
+ // The tool is listed either way; this is the only place an operator
56
+ // can see which answer it gives (#565).
57
+ resumableWrites: opts.resumable !== undefined,
53
58
  // Counts only, from refreshes that already happened — the endpoint
54
59
  // asks no downstream anything, and `connecta doctor` reads it to
55
60
  // report a stale allowlist without a probe of its own (#343).
@@ -71,6 +76,7 @@ export function createFetchHandler(opts) {
71
76
  "/health",
72
77
  ...(opts.ui?.reservedPaths ?? []),
73
78
  ...(opts.ui && opts.activity?.list ? ["/activity"] : []),
79
+ ...(opts.ui && opts.artifactsModule ? ["/artifacts", "/artifacts/*"] : []),
74
80
  ],
75
81
  },
76
82
  ...(opts.deploymentInfo ? { deployment: opts.deploymentInfo } : {}),
@@ -120,6 +126,26 @@ export function createFetchHandler(opts) {
120
126
  const defer = runtimeContext
121
127
  ? runtimeContext.waitUntil.bind(runtimeContext)
122
128
  : undefined;
129
+ // The artifact hostname has no MCP, health, OAuth, or operator routes.
130
+ // Dispatch before MCP origin checks so even a hostile /mcp is a plain 404.
131
+ if (opts.artifactOrigin && url.origin === new URL(opts.artifactOrigin).origin) {
132
+ if (!isArtifactPath(path) || !opts.ui || !opts.artifactsModule) {
133
+ return Effect.succeed(withSecurityHeaders(new Response("Not Found", { status: 404 }), url, path));
134
+ }
135
+ const context = {
136
+ request, url, path, baseUrl: opts.artifactOrigin, opts, defer, runtimeContext,
137
+ };
138
+ return Effect.map(Effect.promise(() => opts.ui.handle(context)), response => withSecurityHeaders(response ?? new Response("Not Found", { status: 404 }), url, path));
139
+ }
140
+ if (opts.artifactOrigin && isArtifactPath(path) && opts.ui && opts.artifactsModule) {
141
+ const target = new URL(opts.artifactOrigin);
142
+ target.pathname = path;
143
+ target.search = url.search;
144
+ return Effect.succeed(withSecurityHeaders(new Response(null, {
145
+ status: 308,
146
+ headers: { Location: target.toString() },
147
+ }), url, path));
148
+ }
123
149
  const originRefusal = routeMcp.rejectOrigin(request);
124
150
  if (originRefusal) {
125
151
  return Effect.succeed(withSecurityHeaders(originRefusal, url, path));
package/dist/skills.js CHANGED
@@ -1,10 +1,24 @@
1
1
  import { boundedEchoText } from "./errors.js";
2
- export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding. Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
2
+ const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding.";
3
+ const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
4
+ /**
5
+ * The MCP instructions of a deployment whose programs pause at writes. The
6
+ * route sentence is its own, not ROUTE's: with writes in programs, "one known
7
+ * write" is the only work left for call_destructive_tool, and an agent told
8
+ * only that programs *may* write reads each write after a lookup as a known
9
+ * one and sends them one prompt at a time.
10
+ */
11
+ export const CONNECTA_INSTRUCTIONS = `Choose a route before discovery. A known-address read needs only call_tool; one known write, call_destructive_tool. Everything else starts with execute_code to discover, call, and return the answer: unknown addresses, reduction, multiple or dependent calls, loops, joins, branches, and writes that follow reads. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding. A program's call to a tool not annotated readOnlyHint: true pauses unsent; resume_execution with the returned token, address, and args approves it and continues the program (approval "tool" covers that tool for the run). Keep catalog inspection top level with search_tools. ${RECOVERY}`;
12
+ /**
13
+ * The instructions of a deployment without resumable writes, whose programs
14
+ * refuse every write: the routing it always had.
15
+ */
16
+ export const READ_ONLY_PROGRAM_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. ${RECOVERY}`;
3
17
  const USAGE_SKILL_BASE = `# Connecta usage
4
18
 
5
19
  ## The surface
6
20
 
7
- Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Read-only discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
21
+ Eight tools: \`execute_code\`, \`resume_execution\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
8
22
 
9
23
  Follow the MCP instructions for routing. Read at most once for syntax or repair.
10
24
 
@@ -25,7 +39,7 @@ Search and call in one run when schemas suffice. For unfamiliar result formats,
25
39
 
26
40
  For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 8), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An \`outputSchemaSource: "observed"\` shape is a hint, not a contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
27
41
 
28
- - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
42
+ - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for reads. These inputs filter discovery; they grant no authority.
29
43
  - Use \`includeSchemas: "json"\` for programmatic schema inspection; compact schemas are text, not objects with \`.properties\`. Check connectorTitle for the account/environment, then address, purpose, annotations, inputs, and outputs. Never select only because a result ranks first or has fewer required inputs.
30
44
  - Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
31
45
  - Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
@@ -34,13 +48,16 @@ For top-level catalog inspection or approval-required discovery, omit \`limit\`
34
48
  - Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
35
49
  - Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
36
50
 
37
- Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
51
+ Only tools explicitly annotated \`readOnlyHint: true\` are reachable unasked. Where the MCP instructions say programs pause, any other call pauses unsent and \`execute_code\` returns \`paused\` with the write and a token; repeat all three unchanged in top-level \`resume_execution\` to approve (\`approval: "tool"\` covers that tool for the run). Replay reruns the program against a journal: make the same calls in the same order. The catalog, credential, admission, and approval gates run below the sandbox; code cannot widen its authority.
38
52
 
39
53
  ## Errors and repair
40
54
 
41
55
  Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Promise rejections retain these fields. Branch on fields, never prose. After a shared argument failure, repair one call before repeating it across other records. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
42
56
 
43
- - \`destructive_tool_requires_approval\`: stop the program and use the returned canonical address with top-level \`call_destructive_tool\`.
57
+ - \`destructive_tool_requires_approval\`: stop and send the returned address through top-level \`call_destructive_tool\`.
58
+ - \`execution_paused\`: the run already stopped at a write; let it end.
59
+ - \`write_outcome_unknown\`: sent but unanswered, never re-sent; check the target.
60
+ - \`execution_expired\`, \`_diverged\`, \`_interrupted\`: check \`writes\`, then rerun.
44
61
  - \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
45
62
  - A truncated direct-call result: follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
46
63
  - Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
@@ -49,12 +66,14 @@ For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` set
49
66
 
50
67
  \`get_result({ id, offset?, maxBytes? })\` returns a one-line JSON header \`{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }\`, a newline, and then the page as raw text, for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1, defaults to the result's cap, and is clamped to it; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the header reports the served offset. Follow \`nextAction\` until \`hasMore\` is false to reassemble pages. An unknown or expired id is an error.
51
68
 
52
- Limits: 20 host calls per run and a 15-second deadline per host call.
69
+ The \`execute_code\` description states this deployment's host-call and write budgets and per-call deadline.
53
70
 
54
71
  ## Runtime portability
55
72
 
56
73
  Portable code uses standard JavaScript builtins, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
57
74
 
75
+ \`Date.now()\` is the run's start and does not advance; \`Math.random()\` is seeded per run.
76
+
58
77
  ## Examples
59
78
 
60
79
  One read-only call at a known address:
@@ -95,8 +114,8 @@ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\`
95
114
  /** Shared Connecta routing guidance, byte-identical across deployments. */
96
115
  export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
97
116
  /** The always-loaded MCP `instructions` string. */
98
- export function instructionsFor() {
99
- return CONNECTA_INSTRUCTIONS;
117
+ export function instructionsFor(resumableWrites) {
118
+ return resumableWrites ? CONNECTA_INSTRUCTIONS : READ_ONLY_PROGRAM_INSTRUCTIONS;
100
119
  }
101
120
  /** True when at least one of `connectors` carries a usage guide. */
102
121
  export function hasConnectorGuides(connectors) {
package/dist/timeout.js CHANGED
@@ -31,6 +31,32 @@ export function withDeadline(operation, options) {
31
31
  catch: (error) => error,
32
32
  }), options));
33
33
  }
34
+ /**
35
+ * Run `operation` under one signal that aborts when any of `signals` does —
36
+ * the MCP call's own and the inbound request's, for the program tools — and
37
+ * detach every listener afterwards, however it ends.
38
+ */
39
+ export async function underAnySignal(signals, operation) {
40
+ const controller = new AbortController();
41
+ const forwarders = signals
42
+ .filter((signal) => signal !== undefined)
43
+ .map((signal) => {
44
+ const forward = () => controller.abort(signal.reason);
45
+ if (signal.aborted)
46
+ forward();
47
+ else
48
+ signal.addEventListener("abort", forward, { once: true });
49
+ return { signal, forward };
50
+ });
51
+ try {
52
+ return await operation(controller.signal);
53
+ }
54
+ finally {
55
+ for (const { signal, forward } of forwarders) {
56
+ signal.removeEventListener("abort", forward);
57
+ }
58
+ }
59
+ }
34
60
  /**
35
61
  * Give one operation a caller-facing deadline and the matching cancellation
36
62
  * signal. The timeout rejects with the stable, labelled error while aborting
@@ -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 {
@@ -240,6 +245,17 @@ export interface Connector {
240
245
  title?: string;
241
246
  /** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
242
247
  kind?: "mcp" | "api";
248
+ /**
249
+ * This connector's own default for its tools that are not explicitly
250
+ * read-only, inside `execute_code`: `"never"` lets a program call them
251
+ * without pausing for approval. Reserved for connectors connecta ships
252
+ * whose writes are cheap to undo (the planned artifacts connector, whose
253
+ * every write is a new immutable version); a deployment's own connectors
254
+ * leave it unset and exempt through `execute.approval`, which overrides
255
+ * this either way. Never read from a downstream catalog — a server cannot
256
+ * exempt its own tools — and never makes a tool read-only anywhere else.
257
+ */
258
+ approval?: "never";
243
259
  description?: string;
244
260
  /**
245
261
  * Max inline result size (bytes) for this connector's tools before
@@ -394,6 +410,8 @@ export interface AdmissionSnapshot {
394
410
  maxQueueSize: number;
395
411
  queueTimeoutMs: number;
396
412
  retryAfterMs: number;
413
+ /** Hard admitted-request lifetime, present only on the /mcp pool. */
414
+ maxDurationMs?: number;
397
415
  active: number;
398
416
  queued: number;
399
417
  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,16 @@ 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
+ }): string;
49
56
  /** Mount the connection UI without enabling any storage or activity module. */
50
57
  export declare function operatorUi(options?: {
51
58
  branding?: ConnectaBranding;
package/dist/ui.js CHANGED
@@ -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
@@ -99,7 +115,7 @@ function jsonForInlineScript(value) {
99
115
  * Every operator page serves this same data-free shell. Connector, credential,
100
116
  * and activity data arrives only through the authenticated `/ui/*` APIs.
101
117
  */
102
- export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections") {
118
+ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections", options = {}) {
103
119
  const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
104
120
  // The Clerk loader's origin. A value that fails the gate is dropped rather
105
121
  // than escaped into the page: the loader tag is simply not emitted, the gate
@@ -197,6 +213,7 @@ ${clerkScript}
197
213
  const AUTH = ${jsonForInlineScript(auth)};
198
214
  const MCP_URL = ${jsonForInlineScript(mcpUrl)};
199
215
  const INITIAL_PAGE = ${jsonForInlineScript(page)};
216
+ const HOME_URL = ${jsonForInlineScript(options.homeUrl ?? "/")};
200
217
  const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
201
218
  const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
202
219
  const PRODUCT_DESCRIPTION = ${stringForInlineScript(brand.description)};
@@ -226,8 +243,17 @@ export function operatorUi(options = {}) {
226
243
  // route here, and no activity module, runs for a path this surface does
227
244
  // not own. The Activity page is the one addition; the server reserves
228
245
  // it only when history is readable, and routeUi checks the same thing.
229
- if (!ownsOperatorPath(reservedPaths, context.path))
246
+ // Artifact pages are the other: they exist only beside the module.
247
+ const artifacts = context.opts.artifactsModule;
248
+ const artifactPath = Boolean(artifacts) && isArtifactPath(context.path);
249
+ if (!artifactPath && !ownsOperatorPath(reservedPaths, context.path))
230
250
  return null;
251
+ if (artifactPath && !operatorPageForPath(context.path)) {
252
+ // The pages' JSON API and the sandboxed frame; the module owns both,
253
+ // and this bundle imports none of it.
254
+ return (await artifacts?.handle(context)) ??
255
+ new Response("Not Found", { status: 404 });
256
+ }
231
257
  const routes = [
232
258
  ...(context.opts.credentialVault ? [routeCredentials] : []),
233
259
  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.0";
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.0";