@zackbart/connecta 0.31.0 → 0.32.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 +34 -0
- package/README.md +15 -13
- package/bin/connecta.mjs +1 -0
- package/bin/version.mjs +1 -1
- package/dist/auth-elicitation.js +1 -1
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +6 -3
- package/dist/config.d.ts +26 -8
- package/dist/config.js +38 -16
- package/dist/connector-access.d.ts +2 -1
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/describe-config.d.ts +6 -3
- package/dist/describe-config.js +3 -2
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.js +4 -3
- package/dist/invocation.js +7 -4
- package/dist/meta-tools.js +17 -19
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -0
- package/dist/operator-ui/contract.d.ts +5 -4
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -1
- package/dist/operator-ui/view.js +1 -0
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +2 -3
- package/dist/providers/_shared/google/workspace.js +1 -1
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +274 -23
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.js +16 -0
- package/dist/providers/notion/rest.js +138 -19
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +286 -24
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +1 -1
- package/dist/providers/sheets/index.js +3 -3
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/routes/mcp.js +30 -10
- package/dist/routes/operator.js +14 -7
- package/dist/routes/ui-config.js +2 -2
- package/dist/server.js +3 -0
- package/dist/skills.js +5 -4
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1 -1
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +38 -23
- package/documentation/auth.md +86 -27
- package/documentation/code-mode.md +81 -72
- package/documentation/deploying.md +10 -5
- package/documentation/integrating.md +56 -4
- package/documentation/meta-tools.md +44 -37
- package/documentation/operating.md +11 -8
- package/documentation/operator-tests.md +2 -3
- package/documentation/operator-ui.md +9 -3
- package/documentation/provider-auth-migration.md +5 -4
- package/documentation/provider-migration-0.29.md +1 -1
- package/examples/worker/AGENTS.md +5 -0
- package/examples/worker/src/connecta.config.ts +5 -0
- package/package.json +5 -1
- package/templates/node/AGENTS.md +6 -4
- package/templates/node/README.md +4 -2
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +5 -0
- package/dist/tool-safety.d.ts +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.32.0 — 2026-10-14
|
|
6
|
+
|
|
7
|
+
Connecta 0.32.0 makes each endpoint simply writable or read-only, and lets a deployment keep chosen callers, such as an agent's token, off plain `/mcp` (#820, #821). A deployment can then give an agent one token and per-person pools, and the agent sees only the pool its client picks for each turn.
|
|
8
|
+
|
|
9
|
+
Every deployment must now choose `readOnly`, and this breaks existing configurations (#822). `readOnly` replaces the pool `trust` setting. The deployment's value applies to `/mcp`, and each pool inherits it unless it sets its own. A deployment that sets neither `readOnly` nor `trust` fails at startup and asks the operator to choose. One that still sets `trust` anywhere fails with a message naming `readOnly`. Replace `trust: "trusted"` with `readOnly: false`, and `trust: "read-only"` with `readOnly: true`. On a read-only endpoint, write tools are left out of search and describe, and every write is refused with `read_only_endpoint` before validation or dispatch, whether it comes from `call_tool`, `call_destructive_tool` or a program. That includes `call_destructive_tool`, which a read-only pool used to allow with host approval. On a writable endpoint, `call_destructive_tool` and programs write, and the MCP client owns approval. Guides are documentation and are not filtered, so a guide may name a tool the endpoint refuses. A deployment that set no `trust` used to let programs read only and allow host-approved direct writes. Choose `readOnly: true` to stop all writes, or `readOnly: false` to let programs write too. A pool that relied on the old read-only default now inherits the deployment's value, so set `readOnly: true` on every pool that must stay read-only. The Node template and Worker example set `readOnly: false`.
|
|
10
|
+
|
|
11
|
+
A new top-level `grant(identity)` admits callers to plain `/mcp` the way pool grants admit callers to pools (#824). Without it, everyone admitted today is still admitted. Only a literal `true` admits. A refused caller gets the same 404 as a refused or undeclared pool, and can still open any pool whose own grant admits it. Result paging and the operator UI's catalog and config reads apply the same grant, so a refused token cannot read the full catalog through `/ui`. OAuth protected-resource metadata stays public, so MCP clients can still discover where to sign in. Deployments that don't set `grant` need no changes.
|
|
12
|
+
|
|
13
|
+
`mixpanel()` gains a REST implementation, `via: "api"`, for what the hosted MCP server cannot answer (#825). It covers raw event export with a size bound, one user's activity stream, profile queries, saved Insights and Funnels reports, retention queries and event import. It authenticates with an operator-managed service account, takes `project_id` per call (with optional `defaultProjectId`) and lists the projects the account can reach. Profile updates are left out because Mixpanel's profile endpoint requires the project token, which service-account credentials alone don't provide. The hosted MCP implementation stays the default, so existing Mixpanel configurations are unchanged. A deployment that wants both declares two connections.
|
|
14
|
+
|
|
15
|
+
The release also adds `excludeApps` to `planningCenter()`, Gmail `get_attachment` and a maintained Wispr Flow provider, and updates the Notion connector to Notion's current limits and errors. CI now runs its checks in parallel, and the redaction tests no longer depend on wall-clock time.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Let deployments configure a grant for plain `/mcp`, independently of named pools. Refusals use the same 404 as refused pools, result paging and operator catalog reads respect the grant while OAuth metadata discovery stays public, and the operator UI and configuration snapshot report whether it is configured.
|
|
20
|
+
- Read Gmail attachment text and small binary files with `get_attachment`. Attachment listings now expose immutable `partId` identifiers for reads across rotated download ids and for inline attachments. Binary content uses standard base64 for Drive uploads; downloads and whole results have explicit size limits.
|
|
21
|
+
|
|
22
|
+
Parsed forwarded attachments include their headers and readable body after checking descendant sizes. MIME charset parameters accept whitespace and quoted values, unsupported charsets are reported, and inconsistent attachment byte counts are refused.
|
|
23
|
+
- Let `mixpanel()` use service-account REST APIs with `via: "api"` for reachable projects, bounded raw event export, profile activity and queries, Insights and Funnels reports, retention queries and event import. Existing hosted MCP configurations remain the default. Retention queries cap intervals at 90 only for day units, as Mixpanel documents.
|
|
24
|
+
- `planningCenter()` accepts `excludeApps`, a list of Planning Center products the connection never reaches, such as `["giving"]` for a deployment that keeps agents away from donor and financial data. An excluded product's named tools leave the catalog; the description, usage guide, and version pins stop naming it; and `pco_api_get` and `pco_api_mutate` refuse its paths with `provider_permission_denied` before sending, checked against the exact URL the transport would fetch. While any product is excluded, a hatch path must be literal (no dot segments, empty segments, backslashes, whitespace, or percent-encoded delimiters), and the connection also removes the People list tools and refuses People lists, list results, rules, and conditions by path or `include`, since a list's conditions can draw on any product; refuses webhook event and delivery payloads and any change to an existing webhook subscription; and accepts a new subscription only to a reachable product's event. Any product except People can be excluded; an unknown name or People throws at construction. The default excludes nothing and is unchanged. Exclusion does not hide records copied into a reachable product by the church's automations or staff, such as People workflow cards a Giving automation creates for first-time donors, notes, or custom fields; for a hard boundary, use a personal access token from a user with No Access in the excluded product, since Planning Center sets permissions per product for each user, and treat `excludeApps` as defense in depth.
|
|
25
|
+
- Add a maintained Wispr Flow hosted-MCP provider for OAuth access to Notetaker meetings, Scratchpad notes, calendar events and tasks, with reviewed tool classification and account-specific usage guidance.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Run CI checks in parallel, cancel superseded PR runs, and request snapshot updates explicitly while preserving the full check gate.
|
|
30
|
+
- **Breaking:** Each endpoint is now writable or read-only, set by a plain `readOnly` boolean that replaces pool `trust`. The deployment's `readOnly` sets `/mcp` and is required for this release: a deployment that sets neither `readOnly` nor the removed `trust` fails at startup with a configuration error asking the operator to choose `readOnly: true` or `readOnly: false`. Each pool inherits the deployment value unless it sets its own, in either direction. The client owns approval. A writable endpoint (`false`) behaves as a `trusted` pool did: `call_destructive_tool` and `execute_code` programs write (program writes stay capped by `execute.maxWrites`), `call_tool` still refuses writes, and `execute_code` is annotated `readOnlyHint: false, destructiveHint: true`. On a read-only endpoint (`true`), write tools are absent from `search_tools` results, `connecta.search`, and `connecta.describe`, and every write call through `call_tool`, `call_destructive_tool`, or a program is refused before validation or dispatch with the new code `read_only_endpoint` (`retryable: false`, no `nextAction`); reads still run, and `execute_code` is annotated `readOnlyHint: true, destructiveHint: false`. Guides are documentation and are not filtered: provider `SKILL.md` and usage guides, surfaced through `queryAnalysis.guideSummary`, `skills`, `connecta.skill`, and skill `resources/read`, may still name tools a read-only endpoint refuses. `tools/list` stays six tools on both. Resource reads, grants, `connectorAccess` (including guarded `requireReadOnly` grants), classification, and `execute.maxWrites` are unchanged. `describeConfig()` and the operator UI report `readOnly` instead of `trust`, and programs no longer receive `destructive_tool_requires_approval`, which remains `call_tool`'s refusal of a write on a writable endpoint.
|
|
31
|
+
|
|
32
|
+
To migrate, set `readOnly` explicitly in deployment code and remove every `trust`; a config that still sets `trust`, on the deployment or a pool, fails at construction with a message naming `readOnly`, and one that sets no `readOnly` fails too. Replace `trust: "trusted"` with `readOnly: false`. Replace `trust: "read-only"` with `readOnly: true`, and note that a read-only endpoint now also refuses writes through `call_destructive_tool`; a pool that should still allow host-approved direct writes but no program writes has no equivalent and must choose. A deployment that set no `trust` was read-only for programs and allowed host-approved direct writes; choose `readOnly: true` to keep it from writing at all, or `readOnly: false` to let programs write too. Pools that relied on the old read-only default inherit the deployment value, so set `readOnly: true` on each pool that must stay read-only under a writable deployment. The Node template and Worker example now set `readOnly: false`, and `connecta init` asks operators to choose.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Make redaction regression tests robust under machine load with CPU scaling, exact output and low-heap checks, without adding production instrumentation.
|
|
37
|
+
- `notion()` over an integration token now splits text longer than Notion's 2,000-character rich-text run limit into consecutive runs in `integration_create_page` and `integration_append_blocks`, instead of sending a request Notion rejects, and refuses text no single value can carry. Failures follow Notion's current contract: a 429 whose reason is `public_api_request_blocked` is no longer retryable, the retry window falls back to the body's `additional_data.retry_after`, a 403 names the workspace block limit, a 5xx on a write that Notion reports as committed names the created object or appended blocks so the caller reads them instead of repeating the write, and a 5xx on a write that carries Notion's `retry_guidance` relays it, unless the operation withholds Notion's error text, and is not retryable. A data source query stopped at Notion's 10,000-result cap reports `incomplete: true` from `integration_query_data_source` and `page.hasMore` without `page.next` from the generic read. Projections name native Notion icons and custom emoji and read an `unsupported` formula or rollup value as `null`. The OAuth connector classifies `notion-ai-search`, `notion-get-self`, `notion-get-tool-access`, and `notion-download-skill` as reads. The guide routes whole-page reads and Markdown writes through the page Markdown endpoints.
|
|
38
|
+
|
|
5
39
|
## 0.31.0 — 2026-10-09
|
|
6
40
|
|
|
7
41
|
Connecta 0.31.0 lets the auth mode choose how Stripe, Notion, Cloudflare and Vercel connect (#801). `auth: { type: "oauth" }` connects the vendor's hosted MCP server, OAuth only. A key connects Connecta's own REST connector with an operator-managed credential: Stripe `apiKey`, Notion `token`, Cloudflare `apiToken` or `globalApiKey`, and Vercel `token`. Each implementation is now complete on its own. This replaces the 0.29 split, in which the hosted server owned writes and the REST surface refused them, so a Cloudflare API token could not change a DNS record. Decision record 0005 explains the choice. Deployments that use none of these four providers need no changes.
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ across them in one task. Changing your connected services does not require
|
|
|
65
65
|
changing the agent's connection. Your deployment decides which services and
|
|
66
66
|
operations each caller can reach.
|
|
67
67
|
|
|
68
|
-
Connecta includes maintained connections for <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, and
|
|
68
|
+
Connecta includes maintained connections for <!-- providers:start -->Basecamp, Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Infisical, Linear, Mixpanel, Notion, Overflow, Planning Center, RevenueCat, Stripe, Tithe.ly, Vercel, and Wispr Flow<!-- providers:end -->.
|
|
69
69
|
Each connection has its own setup guide.
|
|
70
70
|
Stripe, Notion, Cloudflare, and Vercel use their hosted MCP servers when you
|
|
71
71
|
sign in with OAuth, and Connecta's own API connections when you use an API key,
|
|
@@ -76,8 +76,9 @@ HTTP API.
|
|
|
76
76
|
|
|
77
77
|
Google Workspace connections use the account your deployment assigns to each
|
|
78
78
|
signed-in person. A Workspace administrator authorizes them for your domain.
|
|
79
|
-
Gmail reads mail and writes drafts, but does not send mail.
|
|
80
|
-
read, write, and share files, but does not permanently delete
|
|
79
|
+
Gmail reads mail and attachments and writes drafts, but does not send mail.
|
|
80
|
+
Google Drive can read, write, and share files, but does not permanently delete
|
|
81
|
+
them. Available
|
|
81
82
|
operations and account access depend on the integration and your deployment.
|
|
82
83
|
|
|
83
84
|
Large results need not fill the conversation. The agent can filter and
|
|
@@ -85,19 +86,20 @@ summarize them in its program, or read a large result in smaller pieces.
|
|
|
85
86
|
|
|
86
87
|
## Writes, credentials, and access
|
|
87
88
|
|
|
88
|
-
Connecta distinguishes reads from writes
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Connecta distinguishes reads from writes, and each endpoint is either writable
|
|
90
|
+
or read-only. By default an endpoint is writable: your agent can make a change
|
|
91
|
+
with one visible tool call or inside a program, and your agent's host decides
|
|
92
|
+
whether to ask for approval; it sees the program tool as a write. You can mark
|
|
93
|
+
the whole deployment or a single endpoint read-only, and then nothing there can
|
|
94
|
+
change anything: search leaves write tools out and every write is refused. You
|
|
95
|
+
make this choice explicitly for each deployment. Neither choice gives a caller
|
|
96
|
+
access to additional services.
|
|
93
97
|
|
|
94
98
|
```mermaid
|
|
95
99
|
flowchart TB
|
|
96
|
-
Change["Your agent wants to change something"] --> Choice{"
|
|
97
|
-
Choice -->|"
|
|
98
|
-
Choice -->|"
|
|
99
|
-
Separate --> Host["Your agent's host handles approval"]
|
|
100
|
-
Program --> Host
|
|
100
|
+
Change["Your agent wants to change something"] --> Choice{"Is this endpoint<br/>read-only?"}
|
|
101
|
+
Choice -->|"Explicit choice: yes"| Refused["Refused; nothing is sent"]
|
|
102
|
+
Choice -->|"Default: no"| Host["Your agent's host handles approval"]
|
|
101
103
|
Host --> Access["Connecta checks the caller's access"]
|
|
102
104
|
Access --> Services["The connected service receives the change"]
|
|
103
105
|
|
package/bin/connecta.mjs
CHANGED
|
@@ -77,6 +77,7 @@ async function init() {
|
|
|
77
77
|
console.log("Next:");
|
|
78
78
|
console.log(` ${shellCd(target)}`);
|
|
79
79
|
console.log(" npm install");
|
|
80
|
+
console.log(" # Choose readOnly in src/connecta.config.ts: false writes with client approval, true refuses writes.");
|
|
80
81
|
console.log(' npm run --silent provision-token -- "local-machine"');
|
|
81
82
|
console.log(" # Save the returned cta_ token privately for your MCP client and doctor.");
|
|
82
83
|
console.log(" npm start");
|
package/bin/version.mjs
CHANGED
package/dist/auth-elicitation.js
CHANGED
|
@@ -148,7 +148,7 @@ export class AuthElicitation {
|
|
|
148
148
|
requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
|
|
149
149
|
});
|
|
150
150
|
// Recheck every entered read before restarting any part of a program.
|
|
151
|
-
// A
|
|
151
|
+
// A writable endpoint cannot authorize replay of a newly classified write.
|
|
152
152
|
for (const read of previous.reads) {
|
|
153
153
|
const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
|
|
154
154
|
if (!current.ok ||
|
package/dist/catalog-service.js
CHANGED
|
@@ -377,6 +377,7 @@ export class CatalogService {
|
|
|
377
377
|
concurrency;
|
|
378
378
|
searchRoute;
|
|
379
379
|
requestSignal;
|
|
380
|
+
readOnly;
|
|
380
381
|
// The request-scoped catalog cache: one shared read per connector asked
|
|
381
382
|
// about, started by the first asker and joined by every later one. A
|
|
382
383
|
// success stays for the rest of the request, so a search and the call after
|
|
@@ -401,6 +402,7 @@ export class CatalogService {
|
|
|
401
402
|
this.searchRoute = options.searchRoute ?? "search_tools";
|
|
402
403
|
this.requestSignal = options.requestSignal;
|
|
403
404
|
this.defer = options.defer;
|
|
405
|
+
this.readOnly = options.readOnly === true;
|
|
404
406
|
}
|
|
405
407
|
/**
|
|
406
408
|
* Send a caller back to discovery through the surface it can actually reach.
|
|
@@ -460,9 +462,12 @@ export class CatalogService {
|
|
|
460
462
|
// each settling in its input slot: an unavailable catalog is a Failure
|
|
461
463
|
// there, not a failed discovery. `route` names the surface in a timeout.
|
|
462
464
|
// A probe also ends with its request, so no listing outlives the caller
|
|
463
|
-
// that asked for it.
|
|
465
|
+
// that asked for it. On a read-only endpoint a write is absent from search
|
|
466
|
+
// and describe, as if the connector never had it. Guides are not filtered.
|
|
464
467
|
discoveryCatalogs(ids, route) {
|
|
465
|
-
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.
|
|
468
|
+
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly
|
|
469
|
+
? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
|
|
470
|
+
: this.catalog(id, { signal }), {
|
|
466
471
|
...(this.requestSignal ? { signal: this.requestSignal } : {}),
|
|
467
472
|
timeoutMs: this.probeTimeoutMs,
|
|
468
473
|
timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
|
|
@@ -56,8 +56,10 @@ export function describeConfigSources(snapshot, raw) {
|
|
|
56
56
|
source = provided(raw.serverInfo?.[key]);
|
|
57
57
|
else if (group === "urls")
|
|
58
58
|
source = key === "mcpPath" ? "default" : provided(raw[key]);
|
|
59
|
-
else if (group === "
|
|
60
|
-
source = provided(raw.
|
|
59
|
+
else if (group === "hasGrant")
|
|
60
|
+
source = provided(raw.grant);
|
|
61
|
+
else if (group === "readOnly")
|
|
62
|
+
source = provided(raw.readOnly);
|
|
61
63
|
else if (group === "identity")
|
|
62
64
|
source = provided(raw.identity?.[key]);
|
|
63
65
|
else if (group === "storage")
|
|
@@ -67,7 +69,8 @@ export function describeConfigSources(snapshot, raw) {
|
|
|
67
69
|
else if (group === "classification")
|
|
68
70
|
source = provided(raw.classification);
|
|
69
71
|
else if (group === "pools")
|
|
70
|
-
|
|
72
|
+
// An inherited value comes from wherever the deployment's came from.
|
|
73
|
+
source = field === "readOnly" ? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly) : provided(raw.pools);
|
|
71
74
|
else if (group === "modules") {
|
|
72
75
|
const module = key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens;
|
|
73
76
|
const factory = moduleSources[key];
|
package/dist/config.d.ts
CHANGED
|
@@ -76,7 +76,7 @@ declare const execute: {
|
|
|
76
76
|
*/
|
|
77
77
|
watchdogMs: import("./config-schema.js").Field<number, number, true>;
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* Writes one program may send on a writable endpoint, on top of the host-call
|
|
80
80
|
* budget every call already spends. Default 10.
|
|
81
81
|
*/
|
|
82
82
|
maxWrites: import("./config-schema.js").Field<number, number, true>;
|
|
@@ -183,8 +183,13 @@ declare const identity: {
|
|
|
183
183
|
personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
|
|
184
184
|
};
|
|
185
185
|
declare const pool: {
|
|
186
|
-
/**
|
|
187
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Whether this pool's endpoint refuses every write: write tools are absent
|
|
188
|
+
* from search_tools, tool listings, and describe, and every write call is
|
|
189
|
+
* refused. Inherits the deployment's `readOnly` unless set here, in either
|
|
190
|
+
* direction.
|
|
191
|
+
*/
|
|
192
|
+
readOnly: import("./config-schema.js").Field<boolean, boolean | undefined, true>;
|
|
188
193
|
/** Connector ids and exact `connector.tool` addresses in this pool. */
|
|
189
194
|
tools: import("./config-schema.js").Field<readonly string[], readonly string[], false>;
|
|
190
195
|
/**
|
|
@@ -233,18 +238,22 @@ declare const connectaConfig: {
|
|
|
233
238
|
personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
|
|
234
239
|
};
|
|
235
240
|
};
|
|
241
|
+
/**
|
|
242
|
+
* Whether this admitted identity may open plain `/mcp`. Omitted admits
|
|
243
|
+
* everyone; only literal true admits when configured. Other returns and
|
|
244
|
+
* throws receive the same 404 as a refused or undeclared pool.
|
|
245
|
+
*/
|
|
246
|
+
grant: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
|
|
236
247
|
/** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
|
|
237
248
|
pools: import("./config-schema.js").Field<Record<string, {
|
|
238
|
-
|
|
249
|
+
readOnly?: boolean | undefined;
|
|
239
250
|
grant?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
|
|
240
251
|
tools: readonly string[];
|
|
241
252
|
}>, Readonly<Record<string, {
|
|
242
|
-
readonly
|
|
253
|
+
readonly readOnly: boolean | undefined;
|
|
243
254
|
readonly tools: readonly string[];
|
|
244
255
|
readonly grant: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
|
|
245
256
|
}>> | undefined, true>;
|
|
246
|
-
/** Trust of the default /mcp endpoint. Default read-only. */
|
|
247
|
-
trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
|
|
248
257
|
/** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
|
|
249
258
|
classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
|
|
250
259
|
/**
|
|
@@ -394,7 +403,7 @@ declare const connectaConfig: {
|
|
|
394
403
|
*/
|
|
395
404
|
watchdogMs: import("./config-schema.js").Field<number, number, true>;
|
|
396
405
|
/**
|
|
397
|
-
*
|
|
406
|
+
* Writes one program may send on a writable endpoint, on top of the host-call
|
|
398
407
|
* budget every call already spends. Default 10.
|
|
399
408
|
*/
|
|
400
409
|
maxWrites: import("./config-schema.js").Field<number, number, true>;
|
|
@@ -548,6 +557,15 @@ declare const connectaConfig: {
|
|
|
548
557
|
* `customExecutor(myExecutor, { lifecycle: "self-managed" })`.
|
|
549
558
|
*/
|
|
550
559
|
executor: import("./config-schema.js").Field<Executor, Executor, false>;
|
|
560
|
+
/**
|
|
561
|
+
* Required. Whether the default /mcp endpoint refuses every write. `true`:
|
|
562
|
+
* write tools are absent from search_tools, tool listings, and describe,
|
|
563
|
+
* and call_tool, call_destructive_tool, and program writes are refused.
|
|
564
|
+
* `false`: call_destructive_tool and programs may write, and the MCP
|
|
565
|
+
* client owns approval. Pools inherit it unless they set their own. It has
|
|
566
|
+
* no default while deployments migrate from `trust` (#820).
|
|
567
|
+
*/
|
|
568
|
+
readOnly: import("./config-schema.js").Field<boolean, boolean, false>;
|
|
551
569
|
};
|
|
552
570
|
/** A deployment's whole configuration, passed to `createConnecta`. */
|
|
553
571
|
export type ConnectaConfig = ConfigInput<typeof connectaConfig>;
|
package/dist/config.js
CHANGED
|
@@ -134,7 +134,7 @@ const execute = {
|
|
|
134
134
|
*/
|
|
135
135
|
watchdogMs: whole({ min: 1, default: D.execute.watchdogMs }),
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
137
|
+
* Writes one program may send on a writable endpoint, on top of the host-call
|
|
138
138
|
* budget every call already spends. Default 10.
|
|
139
139
|
*/
|
|
140
140
|
maxWrites: whole({ min: 1, default: D.execute.maxWrites }),
|
|
@@ -195,17 +195,21 @@ const identity = {
|
|
|
195
195
|
/** Connecting or changing the caller's personal account. Defaults to none. */
|
|
196
196
|
personalConnection: fn(),
|
|
197
197
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
198
|
+
/** The choice every deployment makes explicitly while `trust` migrates (#820). */
|
|
199
|
+
const READ_ONLY_REQUIRED = (path) => `${path} is required. Choose readOnly: true to refuse every write on /mcp, or readOnly: false to let ` +
|
|
200
|
+
"call_destructive_tool and programs write with MCP client approval. Pools inherit it unless they set their own.";
|
|
201
|
+
function assertFlag(value, path) {
|
|
202
|
+
if (typeof value !== "boolean")
|
|
203
|
+
throw new ConfigError(`${path} must be true or false.`);
|
|
204
|
+
}
|
|
206
205
|
const pool = {
|
|
207
|
-
/**
|
|
208
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Whether this pool's endpoint refuses every write: write tools are absent
|
|
208
|
+
* from search_tools, tool listings, and describe, and every write call is
|
|
209
|
+
* refused. Inherits the deployment's `readOnly` unless set here, in either
|
|
210
|
+
* direction.
|
|
211
|
+
*/
|
|
212
|
+
readOnly: opaque({ check: assertFlag }),
|
|
209
213
|
/** Connector ids and exact `connector.tool` addresses in this pool. */
|
|
210
214
|
tools: required(opaque({
|
|
211
215
|
check: (value, path) => {
|
|
@@ -240,7 +244,7 @@ const connectaConfig = {
|
|
|
240
244
|
connectors: required(array(opaque({
|
|
241
245
|
check: (value, path) => {
|
|
242
246
|
if (isObject(value) && Object.hasOwn(value, "approval")) {
|
|
243
|
-
throw new ConfigError(`${path}.approval was removed;
|
|
247
|
+
throw new ConfigError(`${path}.approval was removed; the MCP client owns approval. Set readOnly: true to refuse writes.`);
|
|
244
248
|
}
|
|
245
249
|
if (!isObject(value) ||
|
|
246
250
|
typeof value.id !== "string" ||
|
|
@@ -274,10 +278,14 @@ const connectaConfig = {
|
|
|
274
278
|
}),
|
|
275
279
|
/** Code-derived connection visibility and independent management permissions. */
|
|
276
280
|
identity: object(identity),
|
|
281
|
+
/**
|
|
282
|
+
* Whether this admitted identity may open plain `/mcp`. Omitted admits
|
|
283
|
+
* everyone; only literal true admits when configured. Other returns and
|
|
284
|
+
* throws receive the same 404 as a refused or undeclared pool.
|
|
285
|
+
*/
|
|
286
|
+
grant: fn(),
|
|
277
287
|
/** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
|
|
278
288
|
pools: record(object(pool)),
|
|
279
|
-
/** Trust of the default /mcp endpoint. Default read-only. */
|
|
280
|
-
trust: trust(),
|
|
281
289
|
/** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
|
|
282
290
|
classification: record(record(opaque({
|
|
283
291
|
check: (value, path) => {
|
|
@@ -415,10 +423,21 @@ const connectaConfig = {
|
|
|
415
423
|
'"@zackbart/connecta/quickjs" on Node, or ' +
|
|
416
424
|
"workerExecutor({ loader: env.LOADER }) from " +
|
|
417
425
|
'"@zackbart/connecta/worker" around DynamicWorkerExecutor on Workers.'),
|
|
426
|
+
/**
|
|
427
|
+
* Required. Whether the default /mcp endpoint refuses every write. `true`:
|
|
428
|
+
* write tools are absent from search_tools, tool listings, and describe,
|
|
429
|
+
* and call_tool, call_destructive_tool, and program writes are refused.
|
|
430
|
+
* `false`: call_destructive_tool and programs may write, and the MCP
|
|
431
|
+
* client owns approval. Pools inherit it unless they set their own. It has
|
|
432
|
+
* no default while deployments migrate from `trust` (#820).
|
|
433
|
+
*/
|
|
434
|
+
readOnly: required(opaque({ check: assertFlag }), (path) => READ_ONLY_REQUIRED(path)),
|
|
418
435
|
};
|
|
419
436
|
const CONFIG = object(connectaConfig);
|
|
420
437
|
/** Pause-only options #672 removed with `resume_execution`. */
|
|
421
438
|
const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
|
|
439
|
+
/** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
|
|
440
|
+
const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
|
|
422
441
|
function rejectUnknownOptions(paths) {
|
|
423
442
|
if (paths.length === 0)
|
|
424
443
|
return;
|
|
@@ -431,10 +450,13 @@ function rejectUnknownOptions(paths) {
|
|
|
431
450
|
? "\nMove branding into ui: operatorUi({ branding }) from @zackbart/connecta/ui."
|
|
432
451
|
: "") +
|
|
433
452
|
(paths.includes("ConnectaConfig.execute.approval")
|
|
434
|
-
?
|
|
453
|
+
? "\nexecute.approval was removed. The MCP client owns approval; set readOnly: true on the deployment or a pool to refuse writes."
|
|
435
454
|
: "") +
|
|
436
455
|
(paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
|
|
437
|
-
? "\nPrograms no longer pause at writes, so there is nothing to configure:
|
|
456
|
+
? "\nPrograms no longer pause at writes, so there is nothing to configure: writable endpoints let programs and call_destructive_tool write; readOnly endpoints refuse every write (issue #672). Delete the option."
|
|
457
|
+
: "") +
|
|
458
|
+
(paths.some(isRetiredTrust)
|
|
459
|
+
? '\ntrust was removed (#820); set readOnly, which every deployment now chooses explicitly. trust: "read-only" becomes readOnly: true, which now also refuses call_destructive_tool. trust: "trusted" becomes readOnly: false. A pool inherits the deployment readOnly unless it sets its own.'
|
|
438
460
|
: ""));
|
|
439
461
|
}
|
|
440
462
|
/** Checks that span fields; each value is already individually valid. */
|
|
@@ -2,7 +2,8 @@ import type { ToolAccess } from "./registry.js";
|
|
|
2
2
|
import type { AuthenticatedIdentity } from "./types.js";
|
|
3
3
|
/** A declared pool after construction-time validation. */
|
|
4
4
|
export interface ResolvedPool {
|
|
5
|
-
|
|
5
|
+
/** Resolved: the pool's own `readOnly`, else the deployment's. */
|
|
6
|
+
readOnly: boolean;
|
|
6
7
|
access: ConnectorAccess;
|
|
7
8
|
grant(identity: Readonly<AuthenticatedIdentity>): boolean | Promise<boolean>;
|
|
8
9
|
}
|
|
@@ -56,6 +56,8 @@ interface GuardedResponse {
|
|
|
56
56
|
readonly status: number;
|
|
57
57
|
readonly ok: boolean;
|
|
58
58
|
readonly headers: Headers;
|
|
59
|
+
/** Cancel an unread body without waiting for the vendor to emit a chunk. */
|
|
60
|
+
cancel(): Promise<void>;
|
|
59
61
|
/** The body as bytes. */
|
|
60
62
|
bytes(): Promise<Uint8Array>;
|
|
61
63
|
/** The body decoded as UTF-8. */
|
|
@@ -124,6 +126,13 @@ export interface GuardedFetchOptions {
|
|
|
124
126
|
* absurd response is a fact about the API, not about HTTP.
|
|
125
127
|
*/
|
|
126
128
|
maxResponseBytes: number;
|
|
129
|
+
/**
|
|
130
|
+
* Inspect the exact URL about to be fetched — confined, normalized, with its
|
|
131
|
+
* query encoded — and throw to refuse it. A policy that reads this URL reads
|
|
132
|
+
* what the downstream server will route, not a spelling of it. Runs before
|
|
133
|
+
* authentication, so a refused request never resolves a credential.
|
|
134
|
+
*/
|
|
135
|
+
admit?: (url: URL, request: GuardedRequest) => void;
|
|
127
136
|
}
|
|
128
137
|
/** Sends one already-guarded request on behalf of one call's context. */
|
|
129
138
|
type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) => Promise<Response>;
|
|
@@ -181,6 +181,9 @@ function boundedResponse(provider, response, limit) {
|
|
|
181
181
|
status: response.status,
|
|
182
182
|
ok: response.ok,
|
|
183
183
|
headers: response.headers,
|
|
184
|
+
cancel: async () => {
|
|
185
|
+
await response.body?.cancel();
|
|
186
|
+
},
|
|
184
187
|
bytes: async () => new Uint8Array(),
|
|
185
188
|
text: async () => "",
|
|
186
189
|
json: emptyJson,
|
|
@@ -228,6 +231,9 @@ function boundedResponse(provider, response, limit) {
|
|
|
228
231
|
status: response.status,
|
|
229
232
|
ok: response.ok,
|
|
230
233
|
headers: response.headers,
|
|
234
|
+
cancel: async () => {
|
|
235
|
+
await response.body?.cancel();
|
|
236
|
+
},
|
|
231
237
|
bytes,
|
|
232
238
|
text,
|
|
233
239
|
json,
|
|
@@ -307,6 +313,7 @@ export function guardedFetch(options) {
|
|
|
307
313
|
throw new Error(`A ${provider} request cannot carry both a JSON body and a raw body.`);
|
|
308
314
|
}
|
|
309
315
|
const url = confinedUrl(provider, base, basePath, request);
|
|
316
|
+
options.admit?.(url, request);
|
|
310
317
|
const headers = { ...constantHeaders };
|
|
311
318
|
for (const [name, value] of Object.entries(request.headers ?? {})) {
|
|
312
319
|
if (value !== undefined)
|
|
@@ -86,8 +86,10 @@ export interface ConnectaConfigDescription {
|
|
|
86
86
|
execute: Record<keyof typeof CONFIG_DEFAULTS.execute, DescribedLimit>;
|
|
87
87
|
requests: Record<keyof typeof CONFIG_DEFAULTS.admission.requests, DescribedLimit>;
|
|
88
88
|
};
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
|
|
90
|
+
readOnly: boolean;
|
|
91
|
+
/** Whether deployment code configures a grant for plain `/mcp`. */
|
|
92
|
+
hasGrant: boolean;
|
|
91
93
|
classification: Record<string, Record<string, "read" | "write">>;
|
|
92
94
|
/** Inbound auth providers in the order they are tried. */
|
|
93
95
|
auth: Array<{
|
|
@@ -96,12 +98,13 @@ export interface ConnectaConfigDescription {
|
|
|
96
98
|
ui?: string;
|
|
97
99
|
}>;
|
|
98
100
|
identity: Record<"connectorAccess" | "activityAccess" | "credentialAdministration" | "accessTokenManagement" | "personalConnection", "default" | "custom">;
|
|
101
|
+
/** `readOnly` is resolved: the pool's own value, else the deployment's. */
|
|
99
102
|
pools: Array<{
|
|
100
103
|
name: string;
|
|
101
104
|
path: string;
|
|
102
105
|
tools: string[];
|
|
103
106
|
hasGrant: boolean;
|
|
104
|
-
|
|
107
|
+
readOnly: boolean;
|
|
105
108
|
}>;
|
|
106
109
|
modules: {
|
|
107
110
|
ui: {
|
package/dist/describe-config.js
CHANGED
|
@@ -240,7 +240,8 @@ export function describeConfig(input) {
|
|
|
240
240
|
execute: limitGroup(config.execute, ownValue(raw, "execute"), Object.keys(D.execute)),
|
|
241
241
|
requests: limitGroup(config.admission.requests, ownValue(ownValue(raw, "admission"), "requests"), Object.keys(D.admission.requests)),
|
|
242
242
|
},
|
|
243
|
-
|
|
243
|
+
readOnly: config.readOnly,
|
|
244
|
+
hasGrant: typeof config.grant === "function",
|
|
244
245
|
classification: structuredClone(config.classification ?? {}),
|
|
245
246
|
auth: config.auth.map((provider) => ({
|
|
246
247
|
kind: String(provider.kind),
|
|
@@ -249,7 +250,7 @@ export function describeConfig(input) {
|
|
|
249
250
|
})),
|
|
250
251
|
identity: Object.fromEntries(IDENTITY_KEYS.map((key) => [key, typeof config.identity[key] === "function" ? "custom" : "default"])),
|
|
251
252
|
pools: Object.entries(config.pools ?? {}).map(([name, pool]) => ({
|
|
252
|
-
|
|
253
|
+
readOnly: pool.readOnly ?? config.readOnly,
|
|
253
254
|
name,
|
|
254
255
|
path: `/mcp/${name}`,
|
|
255
256
|
tools: pool.tools.map(String),
|
package/dist/errors.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "downstream_o
|
|
|
33
33
|
* Adding a member fails typecheck until {@link CLASSIFICATION_CODE_TABLE}
|
|
34
34
|
* lists it.
|
|
35
35
|
*/
|
|
36
|
-
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "destructive_tool_requires_approval" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
36
|
+
export type ClassificationCode = ConnectorCallErrorCode | ExecutorAdmissionErrorCode | "cancelled" | "unknown_address" | "unknown_tool" | "ambiguous_tool_alias" | "catalog_lookup_failed" | "result_too_large" | "destructive_tool_requires_approval" | "read_only_endpoint" | "auth_replay_refused" | "write_outcome_unknown" | "result_processing_failed" | "budget_exceeded" | "executor_failed" | "program_error";
|
|
37
37
|
/** `value` when it is a code connecta assigns, else undefined. */
|
|
38
38
|
export declare function classificationCode(value: unknown): ClassificationCode | undefined;
|
|
39
39
|
/** One bounded, payload-free explanation of an input-schema mismatch. */
|
package/dist/errors.js
CHANGED
|
@@ -28,6 +28,7 @@ const CLASSIFICATION_CODE_TABLE = {
|
|
|
28
28
|
catalog_lookup_failed: true,
|
|
29
29
|
result_too_large: true,
|
|
30
30
|
destructive_tool_requires_approval: true,
|
|
31
|
+
read_only_endpoint: true,
|
|
31
32
|
auth_replay_refused: true,
|
|
32
33
|
write_outcome_unknown: true,
|
|
33
34
|
result_processing_failed: true,
|
package/dist/execute.js
CHANGED
|
@@ -412,6 +412,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
412
412
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
413
413
|
requestScope,
|
|
414
414
|
requestSignal: hostAccessSignal,
|
|
415
|
+
// connecta.search and connecta.describe never show a read-only endpoint's writes.
|
|
416
|
+
readOnly: limits.readOnly,
|
|
415
417
|
// A program that just missed an address cannot call search_tools.
|
|
416
418
|
searchRoute: "connecta.search",
|
|
417
419
|
concurrency: limits.discoveryConcurrency,
|
|
@@ -455,7 +457,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
455
457
|
}
|
|
456
458
|
return value;
|
|
457
459
|
};
|
|
458
|
-
/** Budget and account for writes
|
|
460
|
+
/** Budget and account for writes a writable endpoint admits. */
|
|
459
461
|
const invocationContext = (sending, timeoutMs) => ({
|
|
460
462
|
source: "execute_code",
|
|
461
463
|
sentSecrets,
|
|
@@ -463,9 +465,9 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
463
465
|
...(signal !== undefined ? { requestSignal: signal } : {}),
|
|
464
466
|
...(limits.dispatchController ? { dispatchSignal: limits.dispatchController.signal } : {}),
|
|
465
467
|
unwrapResult: true,
|
|
466
|
-
|
|
468
|
+
readOnly: limits.readOnly,
|
|
467
469
|
processResult: async (value, resolved, secrets, format, raw) => {
|
|
468
|
-
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets,
|
|
470
|
+
const page = (data, valueFormat, wireBytes) => pageProgramResult(registry, baseUrl, resolved, data, valueFormat, secrets, requestScope, wireBytes);
|
|
469
471
|
const envelope = resolved.connector.kind === "mcp" && raw !== null && typeof raw === "object"
|
|
470
472
|
? raw
|
|
471
473
|
: undefined;
|
|
@@ -605,7 +607,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
605
607
|
normalizeTimeoutMs(limits.defaultToolTimeoutMs) ??
|
|
606
608
|
hostCallTimeoutMs;
|
|
607
609
|
const timeoutMs = Math.min(requestedMs, Math.max(1, (limits.programDeadlineAt ?? Infinity) - Date.now()));
|
|
608
|
-
// A
|
|
610
|
+
// A program write settles here — unknown if the call never returned an
|
|
609
611
|
// outcome. Credentials are filtered from arguments; submitted private values are not.
|
|
610
612
|
const sending = {};
|
|
611
613
|
const outcome = yield* invocation
|
|
@@ -656,7 +658,7 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
|
|
|
656
658
|
: Effect.raceAllFirst([reading, fromSignal(hostAccessSignal).pipe(Effect.mapError(cancelled))])).pipe(Effect.onExit((exit) => Effect.sync(() => diagnostics?.recordCatalog(operation, Date.now() - started, Exit.isSuccess(exit), Exit.isSuccess(exit) ? exit.value : undefined))));
|
|
657
659
|
});
|
|
658
660
|
const meta = createMetaTools(registry, baseUrl, {
|
|
659
|
-
|
|
661
|
+
readOnly: limits.readOnly,
|
|
660
662
|
requestScope,
|
|
661
663
|
onResultRead: (dependencies) => limits.resultDependencies?.push(...dependencies),
|
|
662
664
|
});
|
|
@@ -1077,7 +1079,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1077
1079
|
defaultToolTimeoutMs: config.defaultToolTimeoutMs,
|
|
1078
1080
|
programDeadlineAt: Date.now() + watchdog.ms,
|
|
1079
1081
|
defer: config.defer,
|
|
1080
|
-
|
|
1082
|
+
readOnly: config.readOnly,
|
|
1081
1083
|
maxWrites: config.maxWrites,
|
|
1082
1084
|
programWrites,
|
|
1083
1085
|
resultDependencies,
|
|
@@ -1087,7 +1089,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1087
1089
|
return yield* Effect.fail(new ExecutorAdmissionError("executor_cancelled", "Execution was cancelled during sandbox setup."));
|
|
1088
1090
|
}
|
|
1089
1091
|
const admitted = lease;
|
|
1090
|
-
// A
|
|
1092
|
+
// A program write may still be on the wire when the program settles:
|
|
1091
1093
|
// close, so a write gated after this is not sent, then let the ones
|
|
1092
1094
|
// already dispatched finish before the scope aborts them — an
|
|
1093
1095
|
// abandoned write would be one whose outcome nobody knows.
|
|
@@ -1114,7 +1116,7 @@ export function createExecuteTool(registry, baseUrl, executor, logger, activity,
|
|
|
1114
1116
|
emitted,
|
|
1115
1117
|
diagnostics,
|
|
1116
1118
|
program,
|
|
1117
|
-
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets,
|
|
1119
|
+
guardResult: (value, maxChars) => guardProgramReturn(value, registry, baseUrl, [...new Map(resultDependencies.map((dependency) => [JSON.stringify(dependency), dependency])).values()], sentSecrets, logger, maxChars, programWrites.hasSucceeded),
|
|
1118
1120
|
};
|
|
1119
1121
|
const exited = Effect.exit(Effect.scoped(run)).pipe(Effect.flatMap((exit) =>
|
|
1120
1122
|
// Releasing a QuickJS lease ends its child and rejects execute() with
|
|
@@ -1336,7 +1338,7 @@ function connectorInventory(connectors) {
|
|
|
1336
1338
|
return `${prefix}${shown.join(", ")}.`;
|
|
1337
1339
|
return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
|
|
1338
1340
|
}
|
|
1339
|
-
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites,
|
|
1341
|
+
const executeDescription = (emitBudgets, hostLimits, connectorGuides, connectors, maxWrites, readOnly) => `${readOnly ? "One known read: call_tool. Everything else: execute_code. Read-only endpoint: search and describe omit write tools, and every write call is refused." : "One known read: call_tool. One known write: call_destructive_tool. Everything else: execute_code. Writable endpoint: programs may read and write; the client approves."} Limits: ${hostLimits.maxHostCalls} host calls, ${readOnly ? "" : `${maxWrites} writes, `}${hostLimits.hostCallTimeoutMs / 1_000}s/host call.
|
|
1340
1342
|
${connectorInventory(connectors)}
|
|
1341
1343
|
|
|
1342
1344
|
Use async () => { ... }, no arguments. No portable ambient capabilities. API:
|
|
@@ -1383,7 +1385,7 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1383
1385
|
hostCallTimeoutMs: hostLimits.hostCallTimeoutMs,
|
|
1384
1386
|
watchdogMs: ctx.watchdogMs,
|
|
1385
1387
|
defer: ctx.defer,
|
|
1386
|
-
|
|
1388
|
+
readOnly: ctx.readOnly,
|
|
1387
1389
|
maxWrites,
|
|
1388
1390
|
requestScope: ctx.requestScope,
|
|
1389
1391
|
});
|
|
@@ -1391,13 +1393,13 @@ export function registerExecuteTool(server, registry, ctx) {
|
|
|
1391
1393
|
description: executeDescription(emitBudgets, {
|
|
1392
1394
|
...hostLimits,
|
|
1393
1395
|
hostCallTimeoutMs: normalizeTimeoutMs(ctx.defaultToolTimeoutMs) ?? hostLimits.hostCallTimeoutMs,
|
|
1394
|
-
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.
|
|
1396
|
+
}, hasConnectorGuides(connectors), connectors, maxWrites, ctx.readOnly === true),
|
|
1395
1397
|
inputSchema: EXECUTE_INPUT,
|
|
1396
1398
|
outputSchema: EXECUTE_OUTPUT,
|
|
1397
|
-
// The
|
|
1399
|
+
// The client sees execute_code as a write exactly when the endpoint writes.
|
|
1398
1400
|
annotations: {
|
|
1399
|
-
readOnlyHint: ctx.
|
|
1400
|
-
destructiveHint: ctx.
|
|
1401
|
+
readOnlyHint: ctx.readOnly === true,
|
|
1402
|
+
destructiveHint: ctx.readOnly !== true,
|
|
1401
1403
|
openWorldHint: true,
|
|
1402
1404
|
},
|
|
1403
1405
|
}, (args, extra) => {
|