@zackbart/connecta 0.33.0 → 0.34.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.
- package/CHANGELOG.md +65 -0
- package/README.md +100 -128
- package/SECURITY.md +1 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +3 -0
- package/dist/branding.d.ts +7 -0
- package/dist/branding.js +76 -9
- package/dist/catalog-drift.d.ts +30 -26
- package/dist/catalog-drift.js +169 -84
- package/dist/catalog-service.js +16 -2
- package/dist/config-value-sources.js +6 -1
- package/dist/config.d.ts +13 -2
- package/dist/config.js +36 -2
- package/dist/connector-caller.js +2 -1
- package/dist/connectors/api-connector.d.ts +3 -0
- package/dist/connectors/api-connector.js +5 -1
- package/dist/connectors/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +2 -0
- package/dist/connectors/remote-mcp.js +5 -1
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +2 -0
- package/dist/execute.js +1 -11
- package/dist/fix-prompt.js +2 -2
- package/dist/index.js +1 -0
- package/dist/invocation.js +3 -2
- package/dist/membership.d.ts +2 -0
- package/dist/meta-tools.js +3 -2
- package/dist/oauth-callback-outcome.js +2 -2
- package/dist/operator-ui/agents.d.ts +1 -1
- package/dist/operator-ui/agents.js +16 -16
- package/dist/operator-ui/config-summary.js +565 -0
- package/dist/operator-ui/connection-map.js +376 -0
- package/dist/operator-ui/endpoints.js +45 -16
- package/dist/operator-ui/fix-prompts.js +1 -1
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/member-page.js +13 -28
- package/dist/operator-ui/overview.js +123 -0
- package/dist/operator-ui/view.d.ts +26 -22
- package/dist/operator-ui/view.js +130 -96
- package/dist/page-styles.js +5 -3
- package/dist/provider.d.ts +6 -1
- package/dist/provider.js +18 -1
- package/dist/providers/_shared/google/workspace.d.ts +9 -4
- package/dist/providers/_shared/google/workspace.js +7 -3
- package/dist/providers/basecamp/index.d.ts +2 -0
- package/dist/providers/breeze/index.d.ts +2 -0
- package/dist/providers/ccb/index.d.ts +2 -0
- package/dist/providers/ccb/index.js +2 -2
- package/dist/providers/cloudflare/index.d.ts +2 -0
- package/dist/providers/cloudflare/index.js +5 -1
- package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
- package/dist/providers/cloudflare/rest.js +19 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +51 -18
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/intercom/index.d.ts +24 -0
- package/dist/providers/intercom/index.js +121 -0
- package/dist/providers/intercom/skill.generated.js +8 -0
- package/dist/providers/linear/index.js +204 -19
- package/dist/providers/linear/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.d.ts +2 -0
- package/dist/providers/mixpanel/index.js +21 -17
- package/dist/providers/mixpanel/skill.generated.js +3 -3
- package/dist/providers/notion/index.d.ts +2 -0
- package/dist/providers/notion/index.js +1 -1
- package/dist/providers/notion/skill.generated.js +1 -1
- package/dist/providers/overflow/index.d.ts +2 -0
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/overflow/skill.generated.js +2 -2
- package/dist/providers/planning-center/index.d.ts +2 -0
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/skill.generated.js +4 -4
- package/dist/providers/revenuecat/index.d.ts +5 -3
- package/dist/providers/revenuecat/index.js +147 -7
- package/dist/providers/revenuecat/skill.generated.js +3 -3
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/index.d.ts +2 -0
- package/dist/providers/stripe/index.js +50 -30
- package/dist/providers/stripe/skill.generated.js +2 -1
- package/dist/providers/tithely/index.d.ts +2 -0
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/index.d.ts +2 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/openapi.generated.js +391 -374
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +14 -4
- package/dist/registry.d.ts +10 -14
- package/dist/registry.js +77 -62
- package/dist/routes/mcp.js +20 -13
- package/dist/routes/oauth.js +2 -2
- package/dist/routes/ui-data.js +8 -2
- package/dist/skills.js +18 -2
- package/dist/storage/sql.js +5 -1
- package/dist/types.d.ts +36 -11
- package/dist/ui.js +9 -4
- package/dist/usage-guide.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +2 -2
- package/documentation/architecture.md +66 -18
- package/documentation/auth.md +183 -67
- package/documentation/code-mode.md +30 -10
- package/documentation/deploying.md +5 -1
- package/documentation/integrating.md +64 -7
- package/documentation/meta-tools.md +38 -24
- package/documentation/migration-0.33.md +21 -13
- package/documentation/migration-0.34.md +67 -0
- package/documentation/operating.md +1 -1
- package/documentation/operator-tests.md +52 -20
- package/documentation/operator-ui.md +341 -104
- package/documentation/provider-auth-migration.md +22 -0
- package/examples/worker/AGENTS.md +15 -9
- package/examples/worker/README.md +41 -14
- package/examples/worker/src/connecta.config.ts +5 -4
- package/examples/worker/wrangler.jsonc +1 -1
- package/package.json +23 -19
- package/templates/node/AGENTS.md +13 -7
- package/templates/node/README.md +14 -11
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +2 -2
package/dist/operator-ui/view.js
CHANGED
|
@@ -14,21 +14,18 @@ const REGISTRATION_LABELS = {
|
|
|
14
14
|
export function registrationPathLabel(path) {
|
|
15
15
|
return REGISTRATION_LABELS[path];
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* A connector's source column: the maintained provider with the
|
|
19
|
-
* implementation it uses, since a dual provider's `auth` selects either the
|
|
20
|
-
* vendor's hosted MCP server or Connecta's REST connector.
|
|
21
|
-
*/
|
|
22
|
-
export function connectorSourceLabel(source) {
|
|
23
|
-
if (!source.provider)
|
|
24
|
-
return source.kind;
|
|
25
|
-
const implementation = source.kind === "remote-mcp" ? "hosted MCP" : source.kind === "api" ? "REST" : source.kind;
|
|
26
|
-
return `${source.provider} · ${implementation}`;
|
|
27
|
-
}
|
|
28
17
|
/** Pages the nav lists, in dashboard order. */
|
|
29
|
-
export const OPERATOR_PAGES = [
|
|
18
|
+
export const OPERATOR_PAGES = [
|
|
19
|
+
"overview",
|
|
20
|
+
"endpoints",
|
|
21
|
+
"connections",
|
|
22
|
+
"activity",
|
|
23
|
+
"tokens",
|
|
24
|
+
"settings",
|
|
25
|
+
];
|
|
30
26
|
export const PAGE_META = {
|
|
31
|
-
|
|
27
|
+
overview: { path: "/", label: "Overview" },
|
|
28
|
+
endpoints: { path: "/endpoints", label: "Endpoints" },
|
|
32
29
|
connector: { path: "/connectors", label: "Connector" },
|
|
33
30
|
settings: { path: "/settings", label: "Settings" },
|
|
34
31
|
tokens: { path: "/tokens", label: "Tokens" },
|
|
@@ -37,7 +34,8 @@ export const PAGE_META = {
|
|
|
37
34
|
};
|
|
38
35
|
/** Legacy links remain valid, with the browser replacing their URL. */
|
|
39
36
|
export function canonicalOperatorPath(path) {
|
|
40
|
-
return { "/access": "/", "/tools": "/connectors", "/config": "/settings" }[path] ??
|
|
37
|
+
return ({ "/access": "/endpoints", "/tools": "/connectors", "/config": "/settings" }[path] ??
|
|
38
|
+
path);
|
|
41
39
|
}
|
|
42
40
|
/**
|
|
43
41
|
* What each page is for, in one line under its heading — on the gate as well
|
|
@@ -46,7 +44,7 @@ export function canonicalOperatorPath(path) {
|
|
|
46
44
|
*/
|
|
47
45
|
export function pageDescription(page, productDescription) {
|
|
48
46
|
if (page === "activity") {
|
|
49
|
-
return "
|
|
47
|
+
return "Tool calls by caller and result. Arguments and results are not stored.";
|
|
50
48
|
}
|
|
51
49
|
return productDescription;
|
|
52
50
|
}
|
|
@@ -55,12 +53,8 @@ export function pageDescription(page, productDescription) {
|
|
|
55
53
|
* ask, and that this is not an error to retry.
|
|
56
54
|
*/
|
|
57
55
|
export function noAccessCopy(kind, productName) {
|
|
58
|
-
const who = kind === "clerk"
|
|
59
|
-
|
|
60
|
-
: kind === "cloudflare-access"
|
|
61
|
-
? "Cloudflare Access signed you in"
|
|
62
|
-
: "This token is valid";
|
|
63
|
-
return `${who}, but it isn't a maintainer or a member of any endpoint on ${productName}. Ask a maintainer of this deployment to add you to an endpoint, then reload.`;
|
|
56
|
+
const who = kind === "clerk" ? "This account" : kind === "cloudflare-access" ? "This Cloudflare Access identity" : "This token";
|
|
57
|
+
return `${who} has no access to ${productName}. Ask a maintainer to add you to an endpoint, then reload.`;
|
|
64
58
|
}
|
|
65
59
|
/** What the gate says while the session is checked, page by page. */
|
|
66
60
|
export function checkingCopy() {
|
|
@@ -81,8 +75,8 @@ export function failure(message, fix) {
|
|
|
81
75
|
return fix ? { message, tone: "error", fix } : { message, tone: "error" };
|
|
82
76
|
}
|
|
83
77
|
const REFUSED_COPY = {
|
|
84
|
-
oauth_disconnect: "Disconnect
|
|
85
|
-
oauth_reconnect: "Authorization couldn't start.
|
|
78
|
+
oauth_disconnect: "Disconnect failed. The deployment log has the reason.",
|
|
79
|
+
oauth_reconnect: "Authorization couldn't start. The deployment log has the reason.",
|
|
86
80
|
credential_test: "The credential test couldn't run.",
|
|
87
81
|
};
|
|
88
82
|
/**
|
|
@@ -92,20 +86,21 @@ const REFUSED_COPY = {
|
|
|
92
86
|
export function oauthDoneNotice(action, answer, opened = true) {
|
|
93
87
|
if (action === "oauth_disconnect") {
|
|
94
88
|
if (answer?.code === "oauth_revocation_failed")
|
|
95
|
-
return info("Disconnected
|
|
96
|
-
return info("Disconnected.
|
|
89
|
+
return info("Disconnected here. Revoke the grant in the provider's console too.");
|
|
90
|
+
return info("Disconnected.");
|
|
97
91
|
}
|
|
98
92
|
if (answer?.state === "ok")
|
|
99
93
|
return info("Connected.");
|
|
100
|
-
return info(opened
|
|
101
|
-
? "Finish authorizing in the new tab. This page updates when you come back."
|
|
102
|
-
: "Your browser blocked the new tab. Open the authorization page from the link here.");
|
|
94
|
+
return info(opened ? "Finish in the new tab." : "The new tab was blocked. Open the link here.");
|
|
103
95
|
}
|
|
104
96
|
/** A credential Test's result. `ok` is the only part of the answer it reads. */
|
|
105
97
|
export function credentialTestNotice(connectorId, answer) {
|
|
106
98
|
return answer?.ok === true
|
|
107
99
|
? info("Credential is valid.")
|
|
108
|
-
: failure("Credential test failed
|
|
100
|
+
: failure("Credential test failed. The deployment log has the reason.", {
|
|
101
|
+
kind: "credential_test_failed",
|
|
102
|
+
connectorId,
|
|
103
|
+
});
|
|
109
104
|
}
|
|
110
105
|
/**
|
|
111
106
|
* A route that refused one of these actions. A credential Test that found
|
|
@@ -134,19 +129,19 @@ function credentialRefusedCopy(action, status, problem) {
|
|
|
134
129
|
const verb = action === "credential_save" ? "saved" : "removed";
|
|
135
130
|
if (status === 400 || status === 413 || status === 415) {
|
|
136
131
|
return action === "credential_save"
|
|
137
|
-
? "
|
|
138
|
-
: "
|
|
132
|
+
? "Not saved. A value is empty or in the wrong format."
|
|
133
|
+
: "Not removed. Refresh and try again.";
|
|
139
134
|
}
|
|
140
135
|
if (status === 404) {
|
|
141
|
-
return "This connector no longer has a credential slot. Refresh the page
|
|
136
|
+
return "This connector no longer has a credential slot. Refresh the page.";
|
|
142
137
|
}
|
|
143
138
|
if (status === 503) {
|
|
144
|
-
return `
|
|
139
|
+
return `No credential storage is configured, so nothing was ${verb}.`;
|
|
145
140
|
}
|
|
146
|
-
return `
|
|
141
|
+
return `Not ${verb}. Try again, or check the deployment log.`;
|
|
147
142
|
}
|
|
148
143
|
/** An action whose permission or target changed before it was sent. */
|
|
149
|
-
export const STALE_COPY = "This changed before
|
|
144
|
+
export const STALE_COPY = "This changed before sending, so nothing was sent. Try again.";
|
|
150
145
|
/**
|
|
151
146
|
* Every row action's failure, in one voice. The page's own failures — a
|
|
152
147
|
* lapsed session, a denied identity, a dropped connection — read the same for
|
|
@@ -154,13 +149,13 @@ export const STALE_COPY = "This changed before the request was sent, so nothing
|
|
|
154
149
|
*/
|
|
155
150
|
export function actionFailedNotice(action, connectorId, facts, productName) {
|
|
156
151
|
if (facts.kind === "session") {
|
|
157
|
-
return failure("
|
|
152
|
+
return failure("Session ended. Sign in again.");
|
|
158
153
|
}
|
|
159
154
|
if (facts.kind === "forbidden") {
|
|
160
|
-
return failure("You
|
|
155
|
+
return failure("You can't change this connection's authentication.");
|
|
161
156
|
}
|
|
162
157
|
if (facts.kind === "network") {
|
|
163
|
-
return failure(`Couldn't reach ${productName}.
|
|
158
|
+
return failure(`Couldn't reach ${productName}. Try again.`);
|
|
164
159
|
}
|
|
165
160
|
if (facts.kind === "stale")
|
|
166
161
|
return failure(STALE_COPY);
|
|
@@ -171,55 +166,55 @@ export function actionFailedNotice(action, connectorId, facts, productName) {
|
|
|
171
166
|
}
|
|
172
167
|
export function connectorLoadFailureCopy(failure, productName) {
|
|
173
168
|
if (failure === "unavailable")
|
|
174
|
-
return "Session identity is
|
|
169
|
+
return "Session identity is unavailable. Retry.";
|
|
175
170
|
return failure === "session"
|
|
176
|
-
? "
|
|
177
|
-
: `Couldn't reach ${productName}
|
|
171
|
+
? "Session not accepted. Retry, or sign in again."
|
|
172
|
+
: `Couldn't reach ${productName}. Refresh to retry.`;
|
|
178
173
|
}
|
|
179
174
|
/** The signed-in page's answer when `/ui/data` could not be read. */
|
|
180
175
|
export function loadFailureCopy(failure, productName) {
|
|
181
176
|
return {
|
|
182
177
|
title: `Couldn't reach ${productName}`,
|
|
183
178
|
body: failure === "network"
|
|
184
|
-
? "Your browser couldn't connect.
|
|
185
|
-
: "The deployment
|
|
179
|
+
? "Your browser couldn't connect. Retry."
|
|
180
|
+
: "The deployment returned an error. Retry, or check its log.",
|
|
186
181
|
};
|
|
187
182
|
}
|
|
188
183
|
/** A collection page's load failure: fixed words, whatever the route said. */
|
|
189
184
|
export function collectionFailureCopy(_collection, facts, productName) {
|
|
190
185
|
if (facts.kind === "network") {
|
|
191
|
-
return `Couldn't reach ${productName}.
|
|
186
|
+
return `Couldn't reach ${productName}. Retry.`;
|
|
192
187
|
}
|
|
193
188
|
if (facts.kind === "session")
|
|
194
|
-
return "
|
|
189
|
+
return "Session ended. Sign in again.";
|
|
195
190
|
if (facts.kind === "forbidden" || facts.status === 404) {
|
|
196
|
-
return "Activity isn't available to
|
|
191
|
+
return "Activity isn't available to you.";
|
|
197
192
|
}
|
|
198
|
-
return "The deployment
|
|
193
|
+
return "The deployment returned an error. Retry, or check its log.";
|
|
199
194
|
}
|
|
200
195
|
/** The confirm's question, naming the connector by its title. */
|
|
201
196
|
export function confirmCopy(action, name) {
|
|
202
197
|
if (action === "oauth_disconnect") {
|
|
203
198
|
return {
|
|
204
|
-
question: `Disconnect ${name}? Its
|
|
199
|
+
question: `Disconnect ${name}? Its tools stop working until you connect again.`,
|
|
205
200
|
confirm: "Disconnect",
|
|
206
201
|
};
|
|
207
202
|
}
|
|
208
203
|
if (action === "oauth_restart") {
|
|
209
204
|
return {
|
|
210
|
-
question: `Reconnect ${name}?
|
|
205
|
+
question: `Reconnect ${name}? It stops working until you finish in the new tab.`,
|
|
211
206
|
confirm: "Reconnect",
|
|
212
207
|
};
|
|
213
208
|
}
|
|
214
209
|
return {
|
|
215
|
-
question: `Remove ${name}'s credential?
|
|
210
|
+
question: `Remove ${name}'s credential? Its tools stop working until you add a new one.`,
|
|
216
211
|
confirm: "Remove",
|
|
217
212
|
};
|
|
218
213
|
}
|
|
219
214
|
export function accessTokenUnavailableCopy(capability) {
|
|
220
215
|
return capability === undefined
|
|
221
|
-
? "
|
|
222
|
-
: "
|
|
216
|
+
? "Agent tokens aren't configured."
|
|
217
|
+
: "Managing tokens needs an interactive sign-in with permission.";
|
|
223
218
|
}
|
|
224
219
|
/** Who the contract says this viewer is, or null before it loads. */
|
|
225
220
|
export function operatorRole(state) {
|
|
@@ -352,12 +347,12 @@ export function withPage(state, page) {
|
|
|
352
347
|
}
|
|
353
348
|
export function credentialUnavailableCopy(capability) {
|
|
354
349
|
if (capability === "no_slots") {
|
|
355
|
-
return "No
|
|
350
|
+
return "No connector declares a credential slot. Credentials stay in code.";
|
|
356
351
|
}
|
|
357
352
|
if (capability === "vault_not_configured") {
|
|
358
|
-
return "
|
|
353
|
+
return "No credential vault is configured.";
|
|
359
354
|
}
|
|
360
|
-
return "
|
|
355
|
+
return "Managing credentials needs an interactive sign-in. A bearer token can only view them.";
|
|
361
356
|
}
|
|
362
357
|
/**
|
|
363
358
|
* The status badge. An auth-needed connector is named by what it needs, so a
|
|
@@ -394,12 +389,12 @@ export function connectorStatusTone(status) {
|
|
|
394
389
|
* the same kind.
|
|
395
390
|
*/
|
|
396
391
|
const PROBLEM_COPY = {
|
|
397
|
-
connector_unavailable: "Unavailable
|
|
398
|
-
oauth_required: "Needs OAuth authorization
|
|
399
|
-
credential_required: "Needs a credential
|
|
400
|
-
auth_required: "Needs authorization. Its secret
|
|
401
|
-
credential_mismatch: "The stored credential
|
|
402
|
-
catalog_failed: "Connected, but its
|
|
392
|
+
connector_unavailable: "Unavailable. Its status check or catalog load failed or timed out. The deployment log has the error.",
|
|
393
|
+
oauth_required: "Needs OAuth authorization. No grant is stored, or it expired or was revoked.",
|
|
394
|
+
credential_required: "Needs a credential. Nothing usable is stored.",
|
|
395
|
+
auth_required: "Needs authorization. Its secret is set in deployment config, not here.",
|
|
396
|
+
credential_mismatch: "The stored credential doesn't match this connector's fields.",
|
|
397
|
+
catalog_failed: "Connected, but its catalog failed to load, so no tools are served.",
|
|
403
398
|
};
|
|
404
399
|
export function problemCopy(problem) {
|
|
405
400
|
return problem ? (PROBLEM_COPY[problem] ?? null) : null;
|
|
@@ -414,19 +409,41 @@ export function problemTone(problem) {
|
|
|
414
409
|
? "warn"
|
|
415
410
|
: "danger";
|
|
416
411
|
}
|
|
412
|
+
const PROBLEM_LABEL = {
|
|
413
|
+
connector_unavailable: "Unavailable",
|
|
414
|
+
oauth_required: "Authorization needed",
|
|
415
|
+
credential_required: "Credential needed",
|
|
416
|
+
auth_required: "Authorization needed",
|
|
417
|
+
credential_mismatch: "Credential mismatch",
|
|
418
|
+
catalog_failed: "Catalog failed",
|
|
419
|
+
};
|
|
420
|
+
function problemAction(problem) {
|
|
421
|
+
if (problem === "credential_mismatch")
|
|
422
|
+
return "Replace key";
|
|
423
|
+
return problemTone(problem) === "warn" ? "Connect" : "Inspect";
|
|
424
|
+
}
|
|
417
425
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
426
|
+
* The verdict the Connectors list, the Overview, Endpoints and a connector's
|
|
427
|
+
* page all show. The server's problem code comes first, then a failed detail
|
|
428
|
+
* read on this page, then a non-ok status. Catalog drift is informational. A problem
|
|
429
|
+
* outranks the status, so a connector whose catalog failed is never
|
|
430
|
+
* "Connected". A failed read marks its connector `connector_unavailable` in
|
|
431
|
+
* the store, so that one code beside a failed read is the page's own and
|
|
432
|
+
* reads "Couldn't load" instead of blaming the downstream.
|
|
421
433
|
*/
|
|
422
|
-
export function
|
|
423
|
-
if (
|
|
424
|
-
return
|
|
434
|
+
export function connectorVerdict(live, signals = {}) {
|
|
435
|
+
if (live.problem && !(signals.failed && live.problem === "connector_unavailable")) {
|
|
436
|
+
return { tone: problemTone(live.problem), label: PROBLEM_LABEL[live.problem], action: problemAction(live.problem) };
|
|
425
437
|
}
|
|
426
|
-
if (
|
|
427
|
-
return
|
|
438
|
+
if (signals.failed)
|
|
439
|
+
return { tone: "warn", label: "Couldn't load", action: "Inspect" };
|
|
440
|
+
if (live.status === "loading")
|
|
441
|
+
return { tone: "neutral", label: connectorStatusLabel(live.status), action: null };
|
|
442
|
+
if (live.status !== "ok") {
|
|
443
|
+
const tone = connectorStatusTone(live.status) === "warn" ? "warn" : "danger";
|
|
444
|
+
return { tone, label: connectorStatusLabel(live.status), action: tone === "warn" ? "Connect" : "Inspect" };
|
|
428
445
|
}
|
|
429
|
-
return
|
|
446
|
+
return { tone: "ok", label: "Connected", action: null };
|
|
430
447
|
}
|
|
431
448
|
/**
|
|
432
449
|
* The categories, in the order an operator should read them: what the
|
|
@@ -450,7 +467,15 @@ function driftTotal(drift) {
|
|
|
450
467
|
export function driftState(drift) {
|
|
451
468
|
if (!drift)
|
|
452
469
|
return "unavailable";
|
|
453
|
-
|
|
470
|
+
if (driftTotal(drift) > 0)
|
|
471
|
+
return "warning";
|
|
472
|
+
if (drift.reviewedBaseline !== true ||
|
|
473
|
+
drift.verification !== "complete" ||
|
|
474
|
+
!drift.schemaCoverage ||
|
|
475
|
+
drift.schemaCoverage.unpinned > 0 ||
|
|
476
|
+
drift.schemaCoverage.pinned === 0)
|
|
477
|
+
return "unavailable";
|
|
478
|
+
return "clean";
|
|
454
479
|
}
|
|
455
480
|
/** Every category with its count, so a clean report still shows its zeros. */
|
|
456
481
|
export function driftCounts(drift) {
|
|
@@ -464,16 +489,28 @@ export function driftCounts(drift) {
|
|
|
464
489
|
}
|
|
465
490
|
/** One line naming the state and when it was observed. Never what drifted. */
|
|
466
491
|
export function driftSummary(drift) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
const observed = formatDate(drift?.observedAt);
|
|
492
|
+
if (!drift)
|
|
493
|
+
return "No catalog refresh yet.";
|
|
494
|
+
const observed = formatDate(drift.observedAt);
|
|
472
495
|
const when = observed ? ` · observed ${observed}` : "";
|
|
473
|
-
if (state === "clean")
|
|
474
|
-
return `Matches the reviewed manifest${when}`;
|
|
475
496
|
const total = driftTotal(drift);
|
|
476
|
-
|
|
497
|
+
const difference = total
|
|
498
|
+
? `${total} difference${total === 1 ? "" : "s"}${drift.reviewedBaseline === false ? "" : " from the reviewed manifest"} · `
|
|
499
|
+
: "";
|
|
500
|
+
if (drift.reviewedBaseline === false)
|
|
501
|
+
return `${difference}No reviewed baseline${when}`;
|
|
502
|
+
if (drift.verification === "unavailable")
|
|
503
|
+
return `${difference}Schema verification unavailable${when}`;
|
|
504
|
+
const coverage = drift.schemaCoverage;
|
|
505
|
+
if (!coverage || coverage.pinned === 0)
|
|
506
|
+
return `${difference}Schemas not pinned${when}`;
|
|
507
|
+
if (coverage.unpinned > 0)
|
|
508
|
+
return `${difference}Partial schema coverage (${coverage.pinned} pinned, ${coverage.unpinned} not pinned)${when}`;
|
|
509
|
+
if (driftState(drift) === "clean")
|
|
510
|
+
return `Matches the reviewed manifest${when}`;
|
|
511
|
+
if (!total)
|
|
512
|
+
return `Schema verification unavailable${when}`;
|
|
513
|
+
return `${difference.replace(/ · $/, "")}${when}`;
|
|
477
514
|
}
|
|
478
515
|
/**
|
|
479
516
|
* Only http/https may become a clickable href — the browser half of the gate
|
|
@@ -508,6 +545,7 @@ const ACTOR_KINDS = {
|
|
|
508
545
|
bearer: "Bearer token",
|
|
509
546
|
"cloudflare-access": "Cloudflare Access",
|
|
510
547
|
anonymous: "Anonymous",
|
|
548
|
+
access_token: "Agent token",
|
|
511
549
|
};
|
|
512
550
|
/** How a caller signed in, as a person reads it. An unknown kind is shown as sent. */
|
|
513
551
|
function actorKindLabel(kind) {
|
|
@@ -565,9 +603,10 @@ export function activitySummary(events) {
|
|
|
565
603
|
if (events.length === 0)
|
|
566
604
|
return "";
|
|
567
605
|
const calls = events.filter((event) => event.kind !== "catalog_drift");
|
|
606
|
+
const failed = calls.filter((event) => ["error", "timeout"].includes(activityOutcomeClass(event.outcome))).length;
|
|
568
607
|
const changes = events.length - calls.length;
|
|
569
608
|
const tools = new Set(calls.map((event) => event.address)).size;
|
|
570
|
-
return `${calls.length} loaded call${calls.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}${changes ? ` · ${changes} catalog change${changes === 1 ? "" : "s"}` : ""}`;
|
|
609
|
+
return `${calls.length} loaded call${calls.length === 1 ? "" : "s"} · ${failed} failed · ${tools} tool${tools === 1 ? "" : "s"}${changes ? ` · ${changes} catalog change${changes === 1 ? "" : "s"}` : ""}`;
|
|
571
610
|
}
|
|
572
611
|
// A pause and an approval are neither success nor failure: each gets its own
|
|
573
612
|
// class, and the stylesheet paints neither as an error. Nothing has emitted
|
|
@@ -613,12 +652,17 @@ const REASON_LABELS = {
|
|
|
613
652
|
read_only_endpoint: "endpoint is read-only",
|
|
614
653
|
approval_required: "needed approval",
|
|
615
654
|
auth_required: "needed authorization",
|
|
616
|
-
result_too_large: "result too large
|
|
655
|
+
result_too_large: "result too large",
|
|
617
656
|
timeout: "timed out",
|
|
618
657
|
};
|
|
619
658
|
function reasonLabel(code) {
|
|
620
659
|
return REASON_LABELS[code] ?? code.replaceAll("_", " ");
|
|
621
660
|
}
|
|
661
|
+
/** Why a call stalled or failed, in words; "" when nothing was recorded. */
|
|
662
|
+
export function activityReason(event) {
|
|
663
|
+
const reasons = [event.friction, event.errorCode].filter((code) => Boolean(code)).map(reasonLabel);
|
|
664
|
+
return [...new Set(reasons)].join(" · ");
|
|
665
|
+
}
|
|
622
666
|
/** The one-line detail under an address: where it ran, retries, and why it stalled. */
|
|
623
667
|
export function activityDetail(event) {
|
|
624
668
|
if (event.kind === "catalog_drift")
|
|
@@ -645,27 +689,17 @@ export function credentialProblemCopy(problem) {
|
|
|
645
689
|
if (problem === "credential_mismatch")
|
|
646
690
|
return PROBLEM_COPY.credential_mismatch;
|
|
647
691
|
if (problem === "credential_unreadable") {
|
|
648
|
-
return "Credential status is unavailable. Retry
|
|
692
|
+
return "Credential status is unavailable. Retry before replacing or removing it.";
|
|
649
693
|
}
|
|
650
|
-
return "The stored credential can't be used. Replace it
|
|
651
|
-
}
|
|
652
|
-
export function credentialStateLabel(credential) {
|
|
653
|
-
if (credential.configured === null || credential.problem === "credential_unreadable")
|
|
654
|
-
return "unavailable";
|
|
655
|
-
if (!credential.configured)
|
|
656
|
-
return "not configured";
|
|
657
|
-
const masked = "configured";
|
|
658
|
-
return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;
|
|
694
|
+
return "The stored credential can't be used. Replace it.";
|
|
659
695
|
}
|
|
660
696
|
/** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
|
|
661
697
|
export function gateCopy(kind, signedIn) {
|
|
662
698
|
if (kind === "cloudflare-access") {
|
|
663
|
-
return "
|
|
699
|
+
return "Couldn't check your Cloudflare Access identity. Retry.";
|
|
664
700
|
}
|
|
665
701
|
if (kind !== "clerk") {
|
|
666
|
-
return "Paste an operator
|
|
702
|
+
return "Paste an operator token.";
|
|
667
703
|
}
|
|
668
|
-
return signedIn
|
|
669
|
-
? "Signed in with Clerk, but this account cannot open deployment-wide operator pages."
|
|
670
|
-
: "Sign in with Clerk to open this operator page.";
|
|
704
|
+
return signedIn ? "This Clerk account can't open operator pages." : "Sign in with Clerk.";
|
|
671
705
|
}
|
package/dist/page-styles.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Generated by scripts/build-operator-ui.mjs. Do not edit.
|
|
2
|
-
// Source: src/operator-ui/tokens.css
|
|
2
|
+
// Source: src/operator-ui/tokens.css, src/operator-ui/page.css, and the hashed font assets.
|
|
3
3
|
/** Custom properties and both palettes; styles no element. */
|
|
4
|
-
export const TOKENS_CSS = ":root{color-scheme:light;--
|
|
4
|
+
export const TOKENS_CSS = ":root{color-scheme:light;--sans: \"Hanken Grotesk\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: \"JetBrains Mono\", ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--text-2xs: .6875rem;--text-xs: .78125rem;--text-sm: .84375rem;--text-base: .9375rem;--text-md: 1.0625rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.75rem;--text-3xl: 2.125rem;--weight-medium: 500;--weight-semibold: 620;--weight-bold: 700;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--space-10: 2.5rem;--shell: 73.75rem;--pad: 1.5rem;--gap: 1rem;--topbar: 3.875rem;--radius: 14px;--radius-md: min(var(--radius), max(calc(var(--radius) - 4px), 2px));--radius-sm: min(var(--radius), max(calc(var(--radius) - 7px), 2px));--radius-pill: 999px;--bg: #f6f4ef;--surface: #ffffff;--surface-2: #fbfaf7;--sunk: #f0ede6;--hover: #f5f2ec;--line: #e9e4da;--line-strong: #dcd5c8;--line-field: #938b7d;--text: #1d1a16;--text-2: #5c564c;--text-3: #756e61;--scrim: rgb(20 16 8 / .38);--avatar-fill: var(--text);--avatar-ink: var(--bg);--default-accent: #4c44e0;--accent: var(--default-accent);--accent-strong: color-mix(in srgb, var(--accent) 84%, #000000);--accent-soft: color-mix(in srgb, var(--accent) 9%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 26%, var(--surface));--on-accent: var(--custom-on-accent, #ffffff);--link: var(--accent);--ok: #16774b;--ok-soft: #e4f3ea;--ok-line: #bfe2cd;--warn: #9a5800;--warn-soft: #fcf0da;--warn-line: #f1d7a6;--danger: #bc3a27;--danger-soft: #fbe8e4;--danger-line: #f2c5bc;--shadow-1: 0 1px 2px rgb(40 30 10 / .05), 0 1px 1px rgb(40 30 10 / .03);--shadow-2: 0 1px 2px rgb(40 30 10 / .04), 0 6px 20px -6px rgb(40 30 10 / .1);--shadow-3: 0 24px 60px -16px rgb(30 20 0 / .28), 0 2px 6px rgb(30 20 0 / .06)}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){color-scheme:dark}}html[data-scheme=dark]{color-scheme:dark}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){--bg: #12110f;--surface: #1b1a17;--surface-2: #1f1d1a;--sunk: #16150f;--hover: #24221e;--line: #2c2925;--line-strong: #3a3631;--line-field: #757064;--text: #f3efe7;--text-2: #b5aea2;--text-3: #8a8378;--scrim: rgb(0 0 0 / .6);--avatar-fill: #34312c;--avatar-ink: #e9e4da;--default-accent: #8c86ff;--accent-strong: color-mix(in srgb, var(--accent) 80%, #ffffff);--accent-soft: color-mix(in srgb, var(--accent) 17%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 38%, var(--surface));--on-accent: var(--custom-on-accent, #0f0e1f);--link: var(--custom-accent-text, color-mix(in srgb, var(--accent) 80%, #ffffff));--ok: #5bd08f;--ok-soft: #173326;--ok-line: #24513a;--warn: #f0b354;--warn-soft: #352812;--warn-line: #5a4319;--danger: #ff8a75;--danger-soft: #3a1c16;--danger-line: #5e2c22;--shadow-1: 0 1px 2px rgb(0 0 0 / .4);--shadow-2: 0 1px 2px rgb(0 0 0 / .3), 0 8px 24px -8px rgb(0 0 0 / .5);--shadow-3: 0 30px 80px -20px rgb(0 0 0 / .7)}}html[data-scheme=dark]{--bg: #12110f;--surface: #1b1a17;--surface-2: #1f1d1a;--sunk: #16150f;--hover: #24221e;--line: #2c2925;--line-strong: #3a3631;--line-field: #757064;--text: #f3efe7;--text-2: #b5aea2;--text-3: #8a8378;--scrim: rgb(0 0 0 / .6);--avatar-fill: #34312c;--avatar-ink: #e9e4da;--default-accent: #8c86ff;--accent-strong: color-mix(in srgb, var(--accent) 80%, #ffffff);--accent-soft: color-mix(in srgb, var(--accent) 17%, var(--surface));--accent-line: color-mix(in srgb, var(--accent) 38%, var(--surface));--on-accent: var(--custom-on-accent, #0f0e1f);--link: var(--custom-accent-text, color-mix(in srgb, var(--accent) 80%, #ffffff));--ok: #5bd08f;--ok-soft: #173326;--ok-line: #24513a;--warn: #f0b354;--warn-soft: #352812;--warn-line: #5a4319;--danger: #ff8a75;--danger-soft: #3a1c16;--danger-line: #5e2c22;--shadow-1: 0 1px 2px rgb(0 0 0 / .4);--shadow-2: 0 1px 2px rgb(0 0 0 / .3), 0 8px 24px -8px rgb(0 0 0 / .5);--shadow-3: 0 30px 80px -20px rgb(0 0 0 / .7)}";
|
|
5
5
|
/** Base typography, the shell, masthead, buttons, badges, and messages. */
|
|
6
|
-
export const PAGE_CSS = "*{box-sizing:border-box}html{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:16px;line-height:1.
|
|
6
|
+
export const PAGE_CSS = "*{box-sizing:border-box}html{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility}body{background:var(--bg);font-size:var(--text-base);letter-spacing:-.005em;margin:0;min-height:100vh}:is(h1,h2,h3,p,ul,ol){margin:0;padding:0}:is(h1,h2,h3){font-weight:var(--weight-semibold);line-height:1.25}h1{font-size:var(--text-3xl);font-weight:var(--weight-bold);letter-spacing:-.03em;line-height:1.12}h2{font-size:var(--text-lg);letter-spacing:-.015em}h3{font-size:var(--text-base);letter-spacing:-.01em}:is(ul,ol){list-style:none}a{color:var(--link);text-decoration-thickness:1px;text-underline-offset:3px}button,input,select,textarea{color:inherit;font:inherit;letter-spacing:inherit}code,pre,kbd{font-family:var(--mono);letter-spacing:0}::selection{background:var(--accent-soft)}:is(a,button,input,select,textarea,summary,[tabindex]):focus-visible{border-radius:var(--radius-sm);outline:2px solid var(--link);outline-offset:2px}.skip-link{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-md);box-shadow:var(--shadow-2);color:var(--text);font-weight:var(--weight-semibold);left:var(--pad);padding:.5rem .875rem;position:fixed;text-decoration:none;top:-4rem;z-index:60}.skip-link:focus{top:.75rem}.shell{margin:0 auto;max-width:var(--shell);padding-left:var(--pad);padding-right:var(--pad)}.cap,.meta{color:var(--text-2);font-size:var(--text-sm)}.cap{color:var(--text-3);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:.08em;text-transform:uppercase}.mono{font-family:var(--mono);font-size:.86em}.visually-hidden{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.masthead{backdrop-filter:saturate(1.4) blur(14px);background:color-mix(in srgb,var(--bg) 84%,transparent);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:30}.masthead.shell{max-width:none}.masthead-inner{align-items:center;display:flex;gap:var(--space-4);margin:0 auto;max-width:calc(var(--shell) - 2 * var(--pad));min-height:var(--topbar);width:100%}.mast-nav{align-items:center;display:flex;gap:var(--space-2);min-width:0}.brand-mark{border-radius:var(--radius-sm);display:block;flex:none;height:1.625rem;margin-right:.125rem;object-fit:contain;width:1.625rem}span.brand-mark{background:conic-gradient(from 45deg,var(--text) 0 90deg,transparent 0),radial-gradient(circle closest-side,transparent 0 34%,var(--bg) 38% 58.5%,transparent 62.5%),var(--text)}.navlink{background:none;border:0;border-radius:var(--radius-md);color:var(--text-2);cursor:pointer;font-size:var(--text-base);font-weight:var(--weight-medium);padding:.4375rem .75rem;text-decoration:none}.navlink:hover{background:var(--hover);color:var(--text)}.brand,.product{font-size:var(--text-base);overflow-wrap:anywhere;padding-left:0;padding-right:0;text-decoration:none}.brand{color:var(--text);font-size:1rem;font-weight:var(--weight-bold);letter-spacing:-.02em}.brand:hover,.product:hover{background:none}.product{color:var(--text-2);font-weight:var(--weight-medium)}.product:before{color:var(--line-strong);content:\"/\";font-weight:400;margin-right:.5rem}.page{padding-bottom:6rem;padding-top:var(--space-10)}.lead{margin-bottom:var(--space-8)}.lead-copy{color:var(--text-2);display:grid;font-size:var(--text-md);gap:var(--space-2);margin-top:var(--space-2)}.lead-copy p{max-width:60ch}.btn{align-items:center;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--radius-md);box-shadow:var(--shadow-1);color:var(--text);cursor:pointer;display:inline-flex;font-size:var(--text-sm);font-weight:var(--weight-semibold);gap:.4375rem;justify-content:center;line-height:1.2;min-height:2.25rem;padding:.375rem .875rem;text-decoration:none;transition:background-color .15s,border-color .15s,color .15s;white-space:nowrap}.btn:hover{background:var(--hover)}.btn:active{translate:0 1px}.btn:disabled{cursor:not-allowed;opacity:.5;translate:none}.btn.primary{background:var(--accent);border-color:var(--accent);box-shadow:0 1px 2px color-mix(in srgb,var(--accent) 30%,transparent),inset 0 1px #ffffff24;color:var(--on-accent)}.btn.primary:hover{background:var(--accent-strong);border-color:var(--accent-strong)}.btn.danger{border-color:var(--danger-line);color:var(--danger)}.btn.danger:hover{background:var(--danger-soft)}.btn.quiet{background:transparent;border-color:transparent;box-shadow:none;color:var(--text-2)}.btn.quiet:hover{background:var(--hover);color:var(--text)}.badge{align-items:center;background:var(--sunk);border-radius:var(--radius-pill);color:var(--text-2);display:inline-flex;font-size:var(--text-xs);font-weight:var(--weight-semibold);gap:.375rem;line-height:1.2;min-height:1.5rem;padding:.125rem .5625rem;white-space:nowrap}.badge.ok{background:var(--ok-soft);color:var(--ok)}.badge.warn{background:var(--warn-soft);color:var(--warn)}.badge.danger{background:var(--danger-soft);color:var(--danger)}.badge.accent{background:var(--accent-soft);color:var(--link)}.msg{background:var(--danger-soft);border:1px solid var(--danger-line);border-radius:var(--radius-md);color:var(--text);font-size:var(--text-sm);padding:.75rem 1rem}.msg.warn{background:var(--warn-soft);border-color:var(--warn-line)}.status-page{background:var(--surface);border:1px solid var(--line);border-radius:min(calc(var(--radius) + 4px),calc(var(--radius) * 2));box-shadow:var(--shadow-2);display:grid;gap:var(--space-3);margin:8vh auto 0;max-width:30rem;padding:var(--space-8)}.status-head{align-items:center;display:flex;gap:.625rem}.status-mark{align-items:center;background:var(--sunk);border-radius:var(--radius-md);color:var(--text-2);display:inline-flex;flex:none;height:2.25rem;justify-content:center;width:2.25rem}.status-mark svg{height:1.25rem;width:1.25rem}.status-mark.ok{background:var(--ok-soft);color:var(--ok)}.status-mark.danger{background:var(--danger-soft);color:var(--danger)}.status-label{color:var(--text-3);font-size:var(--text-xs);font-weight:var(--weight-semibold);letter-spacing:.08em;text-transform:uppercase}.status-label.ok{color:var(--ok)}.status-label.danger{color:var(--danger)}.status-page h1{font-size:var(--text-2xl);overflow-wrap:anywhere}.status-page h1+p,.status-copy{color:var(--text-2)}.status-actions{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)}.status-details{border-top:1px solid var(--line);color:var(--text-2);font-size:var(--text-sm);margin-top:var(--space-2);padding-top:var(--space-4)}.status-details>summary{align-items:center;cursor:pointer;display:flex;font-weight:var(--weight-semibold);gap:.5rem;list-style:none;width:fit-content}.status-details>summary::-webkit-details-marker{display:none}.status-details>summary:before{border-bottom:1.5px solid currentColor;border-right:1.5px solid currentColor;content:\"\";height:.375rem;rotate:-45deg;transition:rotate .15s;width:.375rem}.status-details[open]>summary:before{rotate:45deg}.status-details>p{margin-top:var(--space-2)}.status-details pre{background:var(--sunk);border:1px solid var(--line);border-radius:var(--radius-md);color:var(--text-2);font-size:var(--text-xs);line-height:1.6;margin:var(--space-2) 0 0;max-height:18rem;overflow:auto;overflow-wrap:anywhere;padding:.75rem .875rem;white-space:pre-wrap}@media(max-width:51.25rem){:root{--pad: 1rem}h1{font-size:var(--text-2xl)}.page{padding-top:var(--space-8)}.status-page{margin-top:var(--space-4);padding:var(--space-6) var(--space-5)}}";
|
|
7
|
+
/** The operator fonts' hashed paths, which only a deployment with the UI serves. */
|
|
8
|
+
export const PAGE_FONTS = [{ "family": "Hanken Grotesk", "weight": "100 900", "path": "/ui/assets/hanken-grotesk-latin-wght-normal-ZZ3K7K5Y.woff2" }, { "family": "JetBrains Mono", "weight": "100 800", "path": "/ui/assets/jetbrains-mono-latin-wght-normal-VBS632QH.woff2" }];
|
package/dist/provider.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ProviderOptions {
|
|
|
19
19
|
authScope?: "shared" | "personal";
|
|
20
20
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
21
21
|
maxResultBytes?: number;
|
|
22
|
+
/** Serve only read-classified tools on every endpoint. Defaults to false. */
|
|
23
|
+
readOnly?: boolean;
|
|
22
24
|
/** Optional per-runtime downstream call-admission policy. */
|
|
23
25
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
24
26
|
}
|
|
@@ -82,8 +84,11 @@ export declare const CALL_ADMISSION: Omit<Field<unknown, ConnectorCallAdmissionP
|
|
|
82
84
|
parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
|
|
83
85
|
readonly shape: typeof admissionPolicy.shape;
|
|
84
86
|
};
|
|
87
|
+
/** Validate the common connector policy before a deployment can serve. */
|
|
88
|
+
export declare const READ_ONLY: Field<boolean, boolean | undefined, true>;
|
|
85
89
|
/** Options every maintained provider shares. */
|
|
86
90
|
export declare const PROVIDER_COMMON: {
|
|
91
|
+
readOnly: Field<boolean, boolean | undefined, true>;
|
|
87
92
|
callAdmission: Omit<Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
|
|
88
93
|
parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
|
|
89
94
|
readonly shape: typeof admissionPolicy.shape;
|
|
@@ -129,7 +134,7 @@ export interface ProviderContext {
|
|
|
129
134
|
* Common connector options the deployment set, and only those, ready to
|
|
130
135
|
* spread into `remoteMcp()` or `api()` options.
|
|
131
136
|
*/
|
|
132
|
-
readonly connectorOptions: Readonly<Pick<ProviderOptions, "authScope" | "maxResultBytes" | "callAdmission">>;
|
|
137
|
+
readonly connectorOptions: Readonly<Pick<ProviderOptions, "authScope" | "maxResultBytes" | "callAdmission" | "readOnly">>;
|
|
133
138
|
/**
|
|
134
139
|
* Render the maintained skill with this connection's context and the
|
|
135
140
|
* deployment's instructions. Instructions are appended; they never replace
|
package/dist/provider.js
CHANGED
|
@@ -50,9 +50,17 @@ export const CALL_ADMISSION = {
|
|
|
50
50
|
return policy;
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
+
/** Validate the common connector policy before a deployment can serve. */
|
|
54
|
+
export const READ_ONLY = opaque({
|
|
55
|
+
check(value, path) {
|
|
56
|
+
if (typeof value !== "boolean")
|
|
57
|
+
throw new ConfigError(`${path} must be a boolean.`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
53
60
|
/** Options every maintained provider shares. */
|
|
54
61
|
export const PROVIDER_COMMON = {
|
|
55
62
|
...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
|
|
63
|
+
readOnly: READ_ONLY,
|
|
56
64
|
callAdmission: CALL_ADMISSION,
|
|
57
65
|
};
|
|
58
66
|
const PROVIDER_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -127,6 +135,7 @@ export function defineProvider(definition) {
|
|
|
127
135
|
const connector = frozen.create(id, resolved, {
|
|
128
136
|
...(frozen.classify !== undefined ? { classify: frozen.classify } : {}),
|
|
129
137
|
connectorOptions: {
|
|
138
|
+
...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}),
|
|
130
139
|
...(options.authScope !== undefined ? { authScope: options.authScope } : {}),
|
|
131
140
|
...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
|
|
132
141
|
...(options.callAdmission !== undefined ? { callAdmission: options.callAdmission } : {}),
|
|
@@ -178,6 +187,8 @@ function asProvider(provider, shape, id, options, build) {
|
|
|
178
187
|
let connector;
|
|
179
188
|
try {
|
|
180
189
|
options = assertKnownOptions(options, `${factory}(${JSON.stringify(id)})`, shape);
|
|
190
|
+
if (options?.readOnly !== undefined)
|
|
191
|
+
READ_ONLY.parse(options.readOnly, `${factory}(${JSON.stringify(id)}).readOnly`);
|
|
181
192
|
const admission = options == null ? undefined : options.callAdmission;
|
|
182
193
|
if (admission !== undefined)
|
|
183
194
|
CALL_ADMISSION.parse(admission, `${factory}(${JSON.stringify(id)}).callAdmission`);
|
|
@@ -188,12 +199,18 @@ function asProvider(provider, shape, id, options, build) {
|
|
|
188
199
|
}
|
|
189
200
|
const describe = connector.describe?.bind(connector);
|
|
190
201
|
const ownDescribe = Object.getOwnPropertyDescriptor(connector, "describe");
|
|
202
|
+
const ownReadOnly = Object.getOwnPropertyDescriptor(connector, "readOnly");
|
|
191
203
|
// Keep the receiver of prototype methods (including private fields). When
|
|
192
204
|
// the description cannot be replaced, inherit the whole connector instead
|
|
193
205
|
// of copying it: decorators may inherit their id, methods, and review.
|
|
194
|
-
const stamped = Object.isExtensible(connector) &&
|
|
206
|
+
const stamped = Object.isExtensible(connector) &&
|
|
207
|
+
(!ownDescribe || ownDescribe.configurable) &&
|
|
208
|
+
(options.readOnly === undefined || !ownReadOnly || ownReadOnly.configurable)
|
|
195
209
|
? connector
|
|
196
210
|
: Object.create(connector);
|
|
211
|
+
if (options.readOnly !== undefined) {
|
|
212
|
+
Object.defineProperty(stamped, "readOnly", { value: options.readOnly, enumerable: true, configurable: true });
|
|
213
|
+
}
|
|
197
214
|
Object.defineProperty(stamped, "describe", {
|
|
198
215
|
configurable: true,
|
|
199
216
|
enumerable: false,
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* Workspace address, and the provider reads the identity core attached beside
|
|
13
13
|
* the context. An agent, which has no Workspace account, acts instead as the
|
|
14
14
|
* `owner` of the pool it came through, which core copies from that pool's
|
|
15
|
-
* config beside the identity; the mapping never sees an agent
|
|
16
|
-
*
|
|
15
|
+
* config beside the identity; the mapping never sees an agent. In a pool
|
|
16
|
+
* without an owner the connector is absent for agents (core's scoped
|
|
17
|
+
* registry view hides it), and this provider still fails `auth_required` if
|
|
18
|
+
* one reaches it anyway (decisions/0006). A fixed
|
|
17
19
|
* `subject` address is every caller's, owners aside. No tool argument,
|
|
18
20
|
* header, or program can name the subject — every product's base URL is
|
|
19
21
|
* confined beneath `users/me`, or its product's equivalent, so the token's
|
|
@@ -61,7 +63,7 @@ export type { GoogleServiceAccount } from "./service-account.js";
|
|
|
61
63
|
* for a caller who has none — that call then fails closed. It is never called
|
|
62
64
|
* for an open deployment's anonymous requests, for a call no request
|
|
63
65
|
* admitted, or for an agent: an agent acts as the `owner` of the pool it
|
|
64
|
-
* came through, and
|
|
66
|
+
* came through, and never sees the connector in a pool without one. It may be async, for
|
|
65
67
|
* a directory lookup (a Clerk user id to an email, say), and receives the
|
|
66
68
|
* call's cancellation signal for that; it runs once per tool call and should
|
|
67
69
|
* cache what it fetches. A string is one fixed account for every caller who
|
|
@@ -98,6 +100,8 @@ export interface GoogleWorkspaceOptions {
|
|
|
98
100
|
callAdmission?: ConnectorCallAdmissionPolicy;
|
|
99
101
|
/** Connector-specific inline result limit; omit to inherit the deployment. */
|
|
100
102
|
maxResultBytes?: number;
|
|
103
|
+
/** Serve only read-classified tools on every endpoint. Defaults to false. */
|
|
104
|
+
readOnly?: boolean;
|
|
101
105
|
/** API base override for a proxy or test double. */
|
|
102
106
|
baseUrl?: string;
|
|
103
107
|
}
|
|
@@ -142,10 +146,11 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
|
|
|
142
146
|
};
|
|
143
147
|
instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
144
148
|
title: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
149
|
+
readOnly: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
145
150
|
maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
146
151
|
purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
147
|
-
baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
148
152
|
subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
153
|
+
baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
|
|
149
154
|
};
|
|
150
155
|
};
|
|
151
156
|
/** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
|