@zackbart/connecta 0.34.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 +30 -0
- package/README.md +3 -2
- package/bin/version.mjs +1 -1
- package/dist/catalog-drift.d.ts +30 -26
- package/dist/catalog-drift.js +169 -84
- package/dist/catalog-service.js +1 -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 +2 -0
- package/dist/describe-config.js +1 -0
- package/dist/invocation.js +3 -2
- package/dist/meta-tools.js +3 -2
- package/dist/operator-ui/config-summary.js +3 -0
- package/dist/operator-ui/connection-map.js +2 -1
- package/dist/operator-ui/endpoints.js +1 -1
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/overview.js +6 -17
- package/dist/operator-ui/view.d.ts +3 -4
- package/dist/operator-ui/view.js +29 -11
- package/dist/provider.d.ts +6 -1
- package/dist/provider.js +18 -1
- package/dist/providers/_shared/google/workspace.d.ts +3 -0
- package/dist/providers/_shared/google/workspace.js +1 -1
- 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 +1 -1
- 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/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 +1 -1
- package/dist/providers/notion/index.d.ts +2 -0
- package/dist/providers/notion/index.js +1 -1
- package/dist/providers/overflow/index.d.ts +2 -0
- package/dist/providers/planning-center/index.d.ts +2 -0
- 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/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/vercel/index.d.ts +2 -0
- package/dist/providers/vercel/index.js +1 -1
- package/dist/providers/vercel/openapi.generated.js +391 -374
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +14 -4
- package/dist/registry.d.ts +10 -14
- package/dist/registry.js +62 -62
- package/dist/routes/ui-data.js +8 -2
- package/dist/skills.js +8 -2
- package/dist/types.d.ts +18 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +48 -5
- package/documentation/code-mode.md +7 -5
- package/documentation/integrating.md +55 -1
- package/documentation/meta-tools.md +2 -2
- package/documentation/operating.md +1 -1
- package/documentation/operator-tests.md +8 -2
- package/documentation/operator-ui.md +58 -11
- package/documentation/provider-auth-migration.md +22 -0
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
|
@@ -4,7 +4,7 @@ export const skill = {
|
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"oauth": "\n\nTool names, descriptions, argument schemas, and result schemas come from Vercel's live server. Connecta applies release-reviewed classification; reviewed writes stay writes even when Vercel claims they only read.\n\n- Discover the live catalog before assuming a tool exists. Vercel can change the surface independently of a Connecta release, and account features may affect what the authorization can reach.\n- Complete authorization and grant access to the owning team before acting. Listing this server does not prove team access.\n- MCP pagination and arguments follow the live MCP schemas; do not apply REST pagination or argument shapes to them.\n- Diagnose deployments with `get_deployment`, then build logs, runtime error clusters, and runtime logs.\n- `get_project_env` and `filter_project_envs` can return decrypted environment values, so Connecta reviews them as writes, and a read-only endpoint refuses them. Prefer reading variable keys and targets without values.\n- Purchase tools change billing. Read a quote first and carry its price, idempotency key, and requested term into the confirmed purchase unchanged.\n- `get_access_to_vercel_url` creates a temporary access grant. Treat the URL it returns as a credential and do not expose it outside the requested task.\n- `web_fetch_vercel_url` runs application code and is reviewed as destructive; the toolbar reply, reaction, edit, and resolve tools are writes.\n- `deploy_to_vercel` and `import-claude-design-from-url` can create or update live projects. Read the target and deployment mode before invoking them.\n- An `auth_required` failure means this connector's OAuth grant is missing or expired. Run `authorize_connector` for this connector id, then retry.\n",
|
|
7
|
-
"key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete and carry the version search returned: `/v13/deployments/dpl_123`, not `/deployments/dpl_123`. Pass `query` and `body` as JSON objects named as `vercel_api_details` lists them. Every call is checked against the pinned index before it is sent: an unknown path returns the nearest operations, and an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Vercel, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `until` (a millisecond timestamp from `pagination.next`) on most lists, `cursor` on newer ones. Lists belong inside `execute_code`; pass `select` (dot paths such as `deployments.uid` and `deployments.state`) to keep only the fields the question needs.\n- `list_teams` returns team ids and slugs and the connection's default team. `get_deployment_build_logs` reads a deployment's build events without following live output. `get_runtime_logs` collects the runtime log stream for at most `waitMs` or `maxRows`, then stops and says why. Generic reads refuse `follow` and the runtime-log stream, which never end on their own.\n- `vercel_api_upload` sends explicit text or base64 bytes to an octet-stream operation, such as `POST /v2/files` before a deployment (Connecta computes `x-vercel-digest`). `vercel_api_write` sends JSON only.\n- Environment values never come back. `list_project_env_vars` never decrypts. `upsert_project_env_var` (`upsert: false` for create-only), `update_project_env_var`, and `delete_project_env_var` return metadata without values; values are write input only. Generic reads refuse `GET /v1/projects/{idOrName}/env/{id}`, `GET /v10/projects/{idOrName}/env`, `GET /v1/env/{id}`, and any `decrypt`, and Connecta replaces the value of every environment variable in any result with `\"[redacted]\"`, including project reads, shared variables, and audit events.\n- Other secrets read as `[redacted]` in every result: credential-named fields and their whole contents (`token`, `secret`, `password`, `jwt`, `credentials`, `Authorization`, …), protection-bypass secrets (each bypass's metadata stays under a numbered placeholder), deploy hook URLs, drain and webhook headers and signing secrets (including the one a new webhook or drain returns), drain and webhook destination URLs and external route, rewrite, and redirect destinations beyond their scheme and host, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), team invite codes, synced Global Config items, and credentials inside other URLs. Failures of every reviewed operation, and of any operation whose request takes a credential-named field, say only the status and a reviewed code. Read or rotate withheld secrets in the Vercel dashboard. A `[redacted]` field is never the real value; do not send it back.\n- Refused by design: endpoints that mint credentials (access tokens, AI Gateway API keys, project OIDC and trace tokens, Connect and SSO tokens, Edge Config tokens, KMS-signed tokens, Flags SDK keys, protection-bypass secrets, installation credential rotation), Connect authorization requests, project transfer requests (the code lets another team claim the project), KMS signing, a domain move-out (its answer is a transfer token), Global Config items, backups, and tokens, and domain transfer auth codes. Each refusal names its reason.\n- The hosted OAuth connection's documentation search, runtime error clusters, toolbar threads, agent runs, web analytics summaries, purchase quotes, `deploy_to_vercel`, and access grants for protected URLs do not exist with a token. Configure an OAuth connector for those.\n- A `429` carries Vercel's reset time; Vercel meters endpoints separately, so wait before retrying the same operation. An `auth_required` failure means the token is missing, expired, or outside this team: an operator must replace it in this connection in the operator UI, and `authorize_connector` returns that handoff. A rejected argument comes back in Vercel's own words; read it rather than replacing the token.\n- Embedded URL safety uses reviewed paths plus global signing parameters. Generic credential-named query parameters (`code`, `key`, `token`, `sig`, etc.) and credential-looking fragments withhold the destination's entire query/fragment only in deployment `errorMessage`, build-event `payload.text`/`text`, and runtime-log `message` on the reviewed deployment/log operations and vendor error text. Ordinary `code`/`key` links can therefore be false positives in these diagnostics. Deployment metadata and other application content keep those ordinary URL parameters byte for byte. Literal AWS SigV4/Google V4 `X-Amz-*`/`X-Goog-*` Signature, Credential and Security-Token values are withheld in every decoded string before projection. A withheld value is not the original: do not write it back. Deployment creation also inspects decoded `files[].data` when `encoding` is `base64` (one decode bounded by the field's encoded length and the host's request limit; malformed base64 is refused). All writes, including named writes and supported raw/base64/multipart uploads, reject URL destinations whose own query/fragment contains the exact `[redacted]` placeholder, plain or encoded, with `invalid_args` before dispatch. Admission iteratively checks every JSON key and value within the host's accepted request size, without separate depth or node-count ceilings; cyclic direct arguments are refused. A marker outside the destination remains ordinary text. Split strings, encoded host/signing names, bare hosts without `//`, and opaque bearer paths remain outside this scan.\n",
|
|
7
|
+
"key": "\n\n- This is Connecta's REST connector to `api.vercel.com`, holding one operator-managed access token. A default team routes calls; it does not narrow what the token can reach.\n- `vercel_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `vercel_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `vercel_api_read` calls GET operations, plus reviewed POSTs that only read: `POST /v2/observability/query`, registrar availability, price, and search, and `POST /v8/artifacts`. `vercel_api_write` calls every other POST, PUT, PATCH, and DELETE, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete and carry the version search returned: `/v13/deployments/dpl_123`, not `/deployments/dpl_123`. Pass `query` and `body` as JSON objects named as `vercel_api_details` lists them. Every call is checked against the pinned index before it is sent: an unknown path returns the nearest operations, and an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Vercel, so fix the call from that answer.\n- Results arrive as `{ status, data, page? }`. On a list, `page.next` is the cursor and `page.param` the query parameter to pass it as: `until` (a millisecond timestamp from `pagination.next`) on most lists, `cursor` on newer ones. Lists belong inside `execute_code`; pass `select` (dot paths such as `deployments.uid` and `deployments.state`) to keep only the fields the question needs.\n- `list_teams` returns team ids and slugs and the connection's default team. `get_deployment_build_logs` reads a deployment's build events without following live output. `get_runtime_logs` collects the runtime log stream for at most `waitMs` or `maxRows`, then stops and says why. Generic reads refuse `follow` and the runtime-log stream, which never end on their own.\n- `vercel_api_upload` sends explicit text or base64 bytes to an octet-stream operation, such as `POST /v2/files` before a deployment (Connecta computes `x-vercel-digest`). `vercel_api_write` sends JSON only.\n- Environment values never come back. `list_project_env_vars` never decrypts. `upsert_project_env_var` (`upsert: false` for create-only), `update_project_env_var`, and `delete_project_env_var` return metadata without values; values are write input only. Generic reads refuse `GET /v1/projects/{idOrName}/env/{id}`, `GET /v10/projects/{idOrName}/env`, `GET /v1/env/{id}`, and any `decrypt`, and Connecta replaces the value of every environment variable in any result with `\"[redacted]\"`, including project reads, shared variables, and audit events.\n- Other secrets read as `[redacted]` in every result: credential-named fields and their whole contents (`token`, `secret`, `password`, `jwt`, `credentials`, `Authorization`, …), protection-bypass secrets (each bypass's metadata stays under a numbered placeholder), deploy hook URLs, daemon liveness-probe header values and internal endpoint ciphertext, drain and webhook headers and signing secrets (including the one a new webhook or drain returns), drain and webhook destination URLs and external route, rewrite, and redirect destinations beyond their scheme and host, the args and values of every header, cookie, and query rule (route transforms and conditions, firewall conditions), conditional firewall bypass compiled `condition` expressions and their credential match values, team invite codes, synced Global Config items, and credentials inside other URLs. Failures of every reviewed operation, and of any operation whose request takes a credential-named field, say only the status and a reviewed code. Read or rotate withheld secrets in the Vercel dashboard. A `[redacted]` field is never the real value; do not send it back.\n- Refused by design: endpoints that mint credentials (access tokens, AI Gateway API keys, project OIDC and trace tokens, Connect and SSO tokens, Edge Config tokens, KMS-signed tokens, Flags SDK keys, protection-bypass secrets, installation credential rotation), Connect authorization requests, project transfer requests (the code lets another team claim the project), KMS signing, a domain move-out (its answer is a transfer token), Global Config items, backups, and tokens, and domain transfer auth codes. Each refusal names its reason.\n- The hosted OAuth connection's documentation search, runtime error clusters, toolbar threads, agent runs, web analytics summaries, purchase quotes, `deploy_to_vercel`, and access grants for protected URLs do not exist with a token. Configure an OAuth connector for those.\n- A `429` carries Vercel's reset time; Vercel meters endpoints separately, so wait before retrying the same operation. An `auth_required` failure means the token is missing, expired, or outside this team: an operator must replace it in this connection in the operator UI, and `authorize_connector` returns that handoff. A rejected argument comes back in Vercel's own words; read it rather than replacing the token.\n- Embedded URL safety uses reviewed paths plus global signing parameters. Generic credential-named query parameters (`code`, `key`, `token`, `sig`, etc.) and credential-looking fragments withhold the destination's entire query/fragment only in deployment `errorMessage`, build-event `payload.text`/`text`, and runtime-log `message` on the reviewed deployment/log operations and vendor error text. Ordinary `code`/`key` links can therefore be false positives in these diagnostics. Deployment metadata and other application content keep those ordinary URL parameters byte for byte. Literal AWS SigV4/Google V4 `X-Amz-*`/`X-Goog-*` Signature, Credential and Security-Token values are withheld in every decoded string before projection. A withheld value is not the original: do not write it back. Deployment creation also inspects decoded `files[].data` when `encoding` is `base64` (one decode bounded by the field's encoded length and the host's request limit; malformed base64 is refused). All writes, including named writes and supported raw/base64/multipart uploads, reject URL destinations whose own query/fragment contains the exact `[redacted]` placeholder, plain or encoded, with `invalid_args` before dispatch. Admission iteratively checks every JSON key and value within the host's accepted request size, without separate depth or node-count ceilings; cyclic direct arguments are refused. A marker outside the destination remains ordinary text. Split strings, encoded host/signing names, bare hosts without `//`, and opaque bearer paths remain outside this scan.\n",
|
|
8
8
|
"shared": "- Resolve team, project, and deployment ids with list reads; never guess one. Team ids begin `team_`, project ids `prj_`, and deployment ids `dpl_`.\n- Diagnose a deployment by reading its state first (`readyState`, `errorCode`, `errorMessage`), then its build logs, then runtime logs for a deployment that built. Narrow time windows before raising limits.\n- Environment variable changes apply only to future deployments; a new deployment is a separate write. Promoting an existing deployment to production does not rebuild it.\n- Treat every create, update, delete, promotion, cancellation, and purchase as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n"
|
|
9
9
|
}
|
|
10
10
|
};
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// Residual risk, as with Infisical: a secret a person typed into a free-text
|
|
25
25
|
// name, description, log line, or identifier is out of scope.
|
|
26
26
|
// Embedded URL verdict (#812): reviewed paths + global signing parameters.
|
|
27
|
-
// Evidence: pinned index/candidates sha256:
|
|
27
|
+
// Evidence: pinned index/candidates sha256:5a702fd2dc76 identify deployment
|
|
28
28
|
// reads/creation/listing, build events and runtime logs; the operation/path
|
|
29
29
|
// list below scans errorMessage, deployments.errorMessage, payload.text/text
|
|
30
30
|
// and message respectively, both lists and individual named-stream rows.
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
// other application content retain generic query parameters byte for byte.
|
|
34
34
|
// Literal X-Amz-/X-Goog- Signature/Credential/Security-Token values are
|
|
35
35
|
// withheld in every string. Existing structured-capability rules still apply.
|
|
36
|
-
// The
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
// The full pinned response document was reviewed on 2026-10-19, including
|
|
37
|
+
// CI failureReason and task recommendations: no vendor-minted bearer links
|
|
38
|
+
// are documented there. This is not a Vercel-wide host policy.
|
|
39
39
|
import { redact, refuse as refusal, safe, under, } from "../_shared/rest/value-safety.js";
|
|
40
40
|
/** A refusal's reason reads as a sentence about the operation: "It mints …". */
|
|
41
41
|
const refuse = (reason) => refusal(`It ${reason}`);
|
|
@@ -51,6 +51,7 @@ const PROJECT = [
|
|
|
51
51
|
"internalRoutes[].missing[?header].value",
|
|
52
52
|
"security.firewallRoutes[].has[?header].value",
|
|
53
53
|
"security.firewallRoutes[].missing[?header].value",
|
|
54
|
+
"security.firewallRoutes[].response_has[?header].value",
|
|
54
55
|
];
|
|
55
56
|
/** Project metadata whose names contain a credential word. */
|
|
56
57
|
const PROJECT_ALLOW = [
|
|
@@ -112,6 +113,7 @@ const FIREWALL_CONDITIONS = [
|
|
|
112
113
|
];
|
|
113
114
|
/** Every firewall config shape: rules, rulesets, and top-level conditions, active, draft, and versions. */
|
|
114
115
|
const FIREWALL = under(["rules[]", "rulesets[]", "conditions[]"], FIREWALL_CONDITIONS);
|
|
116
|
+
const BYPASS_CONDITIONS = ["condition", "conditionGroup[].conditions[?header].value"];
|
|
115
117
|
const FIREWALL_REASON = "Firewall rules match header, cookie, and query values, which can be credentials a rule checks for (an X-Auth-Key).";
|
|
116
118
|
const TEAM_INVITE = "Teams carry inviteCode, which lets its holder join the team (visible to owners).";
|
|
117
119
|
/**
|
|
@@ -120,6 +122,11 @@ const TEAM_INVITE = "Teams carry inviteCode, which lets its holder join the team
|
|
|
120
122
|
* these operations project it themselves (value-safe environment variables).
|
|
121
123
|
*/
|
|
122
124
|
const VALUE_SAFETY = {
|
|
125
|
+
"GET /ai-gateway/budgets/list": safe("Coverage counts per API key; no key value.", ["defaultCoveredCounts.api-key"]),
|
|
126
|
+
"GET /v8/artifacts/status": safe("Remote-cache read-only status is a flag.", ["readOnly"]),
|
|
127
|
+
"GET /v1/security/firewall/bypass/rules": redact(FIREWALL_REASON, under(["", "rules[]"], BYPASS_CONDITIONS)),
|
|
128
|
+
"POST /v1/security/firewall/bypass/rules": redact("The compiled condition and header, cookie, or query matches can contain stored credentials.", BYPASS_CONDITIONS),
|
|
129
|
+
"DELETE /v1/security/firewall/bypass/rules": safe("Deletes a conditional bypass rule; no response value."),
|
|
123
130
|
"PATCH /aliases/{id}/protection-bypass": refuse("creates a shareable link that bypasses Deployment Protection."),
|
|
124
131
|
"POST /api-keys": refuse("mints an AI Gateway API key and returns it in cleartext."),
|
|
125
132
|
"POST /v1/connect/authorize/{connector}": refuse("starts a Connect authorization for a caller-chosen subject and returns its verifier and device code."),
|
|
@@ -185,6 +192,9 @@ const VALUE_SAFETY = {
|
|
|
185
192
|
"POST /v1/kms/issuers/{issuerId}/sign/token": refuse("signs a token with the issuer's private key."),
|
|
186
193
|
"GET /v1/microfrontends/groups/{groupId}/projects": redact(PROJECT_REASON, under(["projects[]"], PROJECT), under(["projects[]"], PROJECT_ALLOW)),
|
|
187
194
|
"POST /v1/projects/traces/session": refuse("mints a trace session token for a deployment."),
|
|
195
|
+
"GET /v1/projects/{idOrName}/daemons": redact("Daemon liveness probes store custom HTTP header values that may authenticate under arbitrary names; encrypted internal endpoint data is also withheld.", ["daemons.config.livenessProbe.httpGet.httpHeaders.value", "daemons.replicas.items.iroh.publicKeyCiphertext"]),
|
|
196
|
+
"PUT /v1/projects/{idOrName}/daemons/{daemonName}/instances": safe("Returns only an ok boolean after scaling instances."),
|
|
197
|
+
"POST /v1/projects/{idOrName}/daemons/{daemonName}/instances/{instanceId}/restart": safe("Returns only an ok boolean after accepting an instance restart."),
|
|
188
198
|
"POST /v1/projects/{idOrName}/avatar": redact(PROJECT_REASON, PROJECT, PROJECT_ALLOW),
|
|
189
199
|
"DELETE /v1/projects/{idOrName}/avatar": redact(PROJECT_REASON, PROJECT, PROJECT_ALLOW),
|
|
190
200
|
"DELETE /v1/projects/{idOrName}/env": redact(ENV_REASON, PROJECT_ENV),
|
package/dist/registry.d.ts
CHANGED
|
@@ -124,6 +124,8 @@ export interface RegistryView {
|
|
|
124
124
|
/** Age of the complete cached catalog; null for static or unobserved catalogs. */
|
|
125
125
|
catalogAgeMs(id: string): number | null;
|
|
126
126
|
statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ConnectorStatus>;
|
|
127
|
+
/** Rebuild a status with this partition's latest bounded observations, without probing. */
|
|
128
|
+
withCatalogObservations(id: string, baseUrl: string, status: ConnectorStatus): ConnectorStatus;
|
|
127
129
|
invalidateStored(id: string): Promise<void>;
|
|
128
130
|
/** Bind returned OAuth state to its initiating user for either owner scope. */
|
|
129
131
|
bindOAuthHandoff(id: string, authorizationUrl: string, principalKey?: string): Promise<void>;
|
|
@@ -159,13 +161,8 @@ export declare class Registry implements RegistryView {
|
|
|
159
161
|
private readonly catalogObservedAt;
|
|
160
162
|
/** Last payload-free agent catalog access in this runtime. */
|
|
161
163
|
private readonly catalogAccess;
|
|
162
|
-
/**
|
|
163
|
-
private
|
|
164
|
-
/**
|
|
165
|
-
* Schema digest verification per reviewed facts array. Only the registry's
|
|
166
|
-
* own deep-frozen arrays are keys, so an entry stays true while they live.
|
|
167
|
-
*/
|
|
168
|
-
private readonly verifiedFacts;
|
|
164
|
+
/** Bounded tokens isolating count-only observations by catalog identity. */
|
|
165
|
+
private readonly catalogObservationPartitions;
|
|
169
166
|
private readonly observedOutputSchemas;
|
|
170
167
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
171
168
|
readonly maxResultBytes: number;
|
|
@@ -220,11 +217,10 @@ export declare class Registry implements RegistryView {
|
|
|
220
217
|
}): Promise<CallAdmissionPermit>;
|
|
221
218
|
/** Connector totals across root and personal controllers; health removes ids. */
|
|
222
219
|
callAdmissionSnapshot(): Record<string, ConnectorCallAdmissionSnapshot>;
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
*/
|
|
220
|
+
/** The admitted identity fields used by both catalog caching and reporting. */
|
|
221
|
+
private catalogPartition;
|
|
222
|
+
/** Bounded, in-memory tokens; no principal or catalog data is persisted. */
|
|
223
|
+
private catalogObservationPartition;
|
|
228
224
|
private catalogDriftOf;
|
|
229
225
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
230
226
|
closeCallAdmission(): void;
|
|
@@ -268,8 +264,6 @@ export declare class Registry implements RegistryView {
|
|
|
268
264
|
private loadDownstreamTools;
|
|
269
265
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
270
266
|
private acceptToolNames;
|
|
271
|
-
/** Observe intake once, never while serving an older cached listing. */
|
|
272
|
-
private observeDroppedToolNames;
|
|
273
267
|
/**
|
|
274
268
|
* One connector's catalog as served: the cached downstream facts, classified
|
|
275
269
|
* on every read by the connector's `classification`, into fresh objects.
|
|
@@ -287,6 +281,8 @@ export declare class Registry implements RegistryView {
|
|
|
287
281
|
credentialDriftFor(id: string): Promise<string | undefined>;
|
|
288
282
|
/** Age of a complete cache entry; reads never refresh it. */
|
|
289
283
|
catalogAgeMs(id: string): number | null;
|
|
284
|
+
/** Pure lookup shared by status probes and operator catalog refreshes. */
|
|
285
|
+
withCatalogObservations(id: string, baseUrl: string, status: ConnectorStatus, scope?: RegistryScope): ConnectorStatus;
|
|
290
286
|
/** Best-effort connector status for the operator UI. */
|
|
291
287
|
statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, scope?: RegistryScope): Promise<ConnectorStatus>;
|
|
292
288
|
/** Rotate the storage generation; in-flight old readers cannot republish it. */
|
package/dist/registry.js
CHANGED
|
@@ -229,13 +229,8 @@ export class Registry {
|
|
|
229
229
|
catalogObservedAt = new Map();
|
|
230
230
|
/** Last payload-free agent catalog access in this runtime. */
|
|
231
231
|
catalogAccess = new Map();
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Schema digest verification per reviewed facts array. Only the registry's
|
|
236
|
-
* own deep-frozen arrays are keys, so an entry stays true while they live.
|
|
237
|
-
*/
|
|
238
|
-
verifiedFacts = new WeakMap();
|
|
232
|
+
/** Bounded tokens isolating count-only observations by catalog identity. */
|
|
233
|
+
catalogObservationPartitions = new Map();
|
|
239
234
|
observedOutputSchemas;
|
|
240
235
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
241
236
|
maxResultBytes;
|
|
@@ -263,6 +258,9 @@ export class Registry {
|
|
|
263
258
|
if (this.connectors.has(c.id)) {
|
|
264
259
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
265
260
|
}
|
|
261
|
+
if (c.readOnly !== undefined && typeof c.readOnly !== "boolean") {
|
|
262
|
+
throw new Error(`Connector "${c.id}" readOnly must be a boolean.`);
|
|
263
|
+
}
|
|
266
264
|
if (c.authScope !== undefined && c.authScope !== "shared" && c.authScope !== "personal") {
|
|
267
265
|
throw new Error(`Invalid authScope on connector "${c.id}": expected "shared" or "personal"`);
|
|
268
266
|
}
|
|
@@ -332,9 +330,6 @@ export class Registry {
|
|
|
332
330
|
catalogStorage: this.opts.catalogStorage ?? this.opts.storage,
|
|
333
331
|
constructionChecks: false,
|
|
334
332
|
});
|
|
335
|
-
// Counts only, like the connector-wide reviewed drift observation. No
|
|
336
|
-
// principal, credentials, or catalog payload crosses this registry boundary.
|
|
337
|
-
registry.droppedToolNames = this.droppedToolNames;
|
|
338
333
|
if (this.callAdmissionClosed)
|
|
339
334
|
registry.closeCallAdmission();
|
|
340
335
|
this.personalRegistries.set(principalKey, registry);
|
|
@@ -503,13 +498,7 @@ export class Registry {
|
|
|
503
498
|
attachCaller(context, scope?.caller);
|
|
504
499
|
attachCatalogCache(context, {
|
|
505
500
|
storage: this.opts.catalogStorage ?? this.opts.storage,
|
|
506
|
-
partition:
|
|
507
|
-
scope?.principalKey ?? this.opts.credentialOwner ?? null,
|
|
508
|
-
scope?.subjectKey ?? null,
|
|
509
|
-
scope?.caller?.identity ?? null,
|
|
510
|
-
scope?.caller?.authenticated ?? false,
|
|
511
|
-
scope?.caller?.pool ?? null,
|
|
512
|
-
]),
|
|
501
|
+
partition: this.catalogPartition(scope),
|
|
513
502
|
sharedPartition: JSON.stringify([baseUrl, this.opts.publicUrl ?? null, scope?.caller?.pool ?? null]),
|
|
514
503
|
defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
|
|
515
504
|
minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
|
|
@@ -553,26 +542,38 @@ export class Registry {
|
|
|
553
542
|
}
|
|
554
543
|
return Object.fromEntries([...snapshots].map(([id, values]) => [id, aggregateCallAdmissionSnapshots(values)]));
|
|
555
544
|
}
|
|
556
|
-
/**
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
545
|
+
/** The admitted identity fields used by both catalog caching and reporting. */
|
|
546
|
+
catalogPartition(scope) {
|
|
547
|
+
return JSON.stringify([
|
|
548
|
+
scope?.principalKey ?? this.opts.credentialOwner ?? null,
|
|
549
|
+
scope?.subjectKey ?? null,
|
|
550
|
+
scope?.caller?.identity ?? null,
|
|
551
|
+
scope?.caller?.authenticated ?? false,
|
|
552
|
+
scope?.caller?.pool ?? null,
|
|
553
|
+
]);
|
|
554
|
+
}
|
|
555
|
+
/** Bounded, in-memory tokens; no principal or catalog data is persisted. */
|
|
556
|
+
catalogObservationPartition(id, baseUrl, scope, create = false) {
|
|
557
|
+
const key = JSON.stringify([id, this.catalogPartition(scope), baseUrl, this.opts.publicUrl ?? null]);
|
|
558
|
+
let partition = this.catalogObservationPartitions.get(key);
|
|
559
|
+
if (!partition && create) {
|
|
560
|
+
partition = {};
|
|
561
|
+
this.catalogObservationPartitions.set(key, partition);
|
|
562
|
+
if (this.catalogObservationPartitions.size > 1024)
|
|
563
|
+
this.catalogObservationPartitions.delete(this.catalogObservationPartitions.keys().next().value);
|
|
564
|
+
}
|
|
565
|
+
return partition;
|
|
566
|
+
}
|
|
567
|
+
catalogDriftOf(connector, baseUrl, scope) {
|
|
568
|
+
const partition = this.catalogObservationPartition(connector.id, baseUrl, scope);
|
|
569
|
+
const observed = partition ? observedCatalogDrift(connector, partition) : undefined;
|
|
570
|
+
// Plugin reports cannot establish a reviewed baseline or schema coverage
|
|
571
|
+
// that this registry has not checked. Personal plugin seams have no subject
|
|
572
|
+
// parameter and must never bypass the partition above.
|
|
573
|
+
const fallback = !observed && !catalogReviewOf(connector) && connector.authScope !== "personal"
|
|
574
|
+
? connector.catalogDrift?.()
|
|
575
|
+
: undefined;
|
|
576
|
+
return boundedCatalogDrift(observed ?? (fallback ? { ...fallback, reviewedBaseline: false } : undefined));
|
|
576
577
|
}
|
|
577
578
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
578
579
|
closeCallAdmission() {
|
|
@@ -745,7 +746,7 @@ export class Registry {
|
|
|
745
746
|
if (!connector)
|
|
746
747
|
throw new Error(`Unknown connector "${id}"`);
|
|
747
748
|
if (connector.staticTools)
|
|
748
|
-
return markCatalogFreshness(connector.staticTools, Infinity);
|
|
749
|
+
return { tools: markCatalogFreshness(connector.staticTools, Infinity), verification: undefined };
|
|
749
750
|
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
750
751
|
let listed;
|
|
751
752
|
try {
|
|
@@ -766,10 +767,8 @@ export class Registry {
|
|
|
766
767
|
}
|
|
767
768
|
const facts = frozenFacts(structuredClone(tools));
|
|
768
769
|
const accepted = this.acceptToolNames(facts);
|
|
769
|
-
this.observeDroppedToolNames(connector, facts.length - accepted.length);
|
|
770
770
|
const review = catalogReviewOf(connector);
|
|
771
|
-
|
|
772
|
-
await observeReviewedDrift(connector, review, accepted, this.opts.logger);
|
|
771
|
+
const verification = await observeReviewedDrift(connector, review, accepted, this.opts.logger, this.catalogObservationPartition(id, baseUrl, scope, true), facts.length - accepted.length);
|
|
773
772
|
if (catalogFetchedAt(ctx) === undefined) {
|
|
774
773
|
try {
|
|
775
774
|
await storeCustomCatalogFallback(ctx, id, facts);
|
|
@@ -791,19 +790,13 @@ export class Registry {
|
|
|
791
790
|
state: freshUntil > Date.now() ? "fresh" : "stale",
|
|
792
791
|
observedAt: new Date().toISOString(),
|
|
793
792
|
});
|
|
794
|
-
return markCatalogFreshness(
|
|
793
|
+
return { tools: markCatalogFreshness(accepted, freshUntil), verification };
|
|
795
794
|
}
|
|
796
795
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
797
796
|
acceptToolNames(tools) {
|
|
798
797
|
const accepted = tools.filter((tool) => !hasControlCharacters(tool.name));
|
|
799
798
|
return accepted.length === tools.length ? tools : accepted;
|
|
800
799
|
}
|
|
801
|
-
/** Observe intake once, never while serving an older cached listing. */
|
|
802
|
-
observeDroppedToolNames(connector, count) {
|
|
803
|
-
if (count > 0 || this.droppedToolNames.has(connector.id)) {
|
|
804
|
-
this.droppedToolNames.set(connector.id, { count, observedAt: new Date().toISOString() });
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
800
|
/**
|
|
808
801
|
* One connector's catalog as served: the cached downstream facts, classified
|
|
809
802
|
* on every read by the connector's `classification`, into fresh objects.
|
|
@@ -813,7 +806,7 @@ export class Registry {
|
|
|
813
806
|
* the next read.
|
|
814
807
|
*/
|
|
815
808
|
async loadTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
816
|
-
const tools = await this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
|
|
809
|
+
const { tools, verification } = await this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
|
|
817
810
|
const connector = this.connectors.get(id);
|
|
818
811
|
if (!connector)
|
|
819
812
|
throw new Error(`Unknown connector "${id}"`);
|
|
@@ -822,7 +815,7 @@ export class Registry {
|
|
|
822
815
|
this.validateClassification(id, accepted, overrides);
|
|
823
816
|
const review = catalogReviewOf(connector);
|
|
824
817
|
return carryCatalogFreshness(tools, review
|
|
825
|
-
? await classifyCatalog(review, id, accepted, this.opts.logger,
|
|
818
|
+
? await classifyCatalog(review, id, accepted, this.opts.logger, overrides, verification)
|
|
826
819
|
: accepted.map((tool) => this.publishUnreviewedTool(id, tool)));
|
|
827
820
|
}
|
|
828
821
|
publishUnreviewedTool(id, tool) {
|
|
@@ -904,6 +897,20 @@ export class Registry {
|
|
|
904
897
|
const observedAt = this.catalogObservedAt.get(id);
|
|
905
898
|
return observedAt === undefined ? null : Math.max(0, Date.now() - observedAt);
|
|
906
899
|
}
|
|
900
|
+
/** Pure lookup shared by status probes and operator catalog refreshes. */
|
|
901
|
+
withCatalogObservations(id, baseUrl, status, scope) {
|
|
902
|
+
const connector = this.connectors.get(id);
|
|
903
|
+
if (!connector)
|
|
904
|
+
return ownStatus({ state: "error", message: "Unknown connector" });
|
|
905
|
+
const report = this.catalogDriftOf(connector, baseUrl, scope);
|
|
906
|
+
const access = this.catalogAccess.get(id);
|
|
907
|
+
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
908
|
+
// it cannot smuggle payload or another partition's observations (INV-6).
|
|
909
|
+
return Object.assign(boundedStatus(status), {
|
|
910
|
+
...(report ? { catalogDrift: report } : {}),
|
|
911
|
+
...(access ? { catalogAccess: { ...access } } : {}),
|
|
912
|
+
});
|
|
913
|
+
}
|
|
907
914
|
/** Best-effort connector status for the operator UI. */
|
|
908
915
|
async statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
909
916
|
const connector = this.connectors.get(id);
|
|
@@ -917,18 +924,7 @@ export class Registry {
|
|
|
917
924
|
// The report is rebuilt rather than spread through: what the seam returned
|
|
918
925
|
// is third-party output, and status is read by the operator UI and copied
|
|
919
926
|
// into responses.
|
|
920
|
-
const withObservations = (status) =>
|
|
921
|
-
const report = this.catalogDriftOf(connector);
|
|
922
|
-
const access = this.catalogAccess.get(id);
|
|
923
|
-
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
924
|
-
// a connector cannot smuggle payload through either registry-owned
|
|
925
|
-
// observation when this runtime has not made one, nor through its
|
|
926
|
-
// message, which survives only when connecta wrote it (INV-6).
|
|
927
|
-
return Object.assign(boundedStatus(status), {
|
|
928
|
-
...(report ? { catalogDrift: report } : {}),
|
|
929
|
-
...(access ? { catalogAccess: { ...access } } : {}),
|
|
930
|
-
});
|
|
931
|
-
};
|
|
927
|
+
const withObservations = (status) => this.withCatalogObservations(id, baseUrl, status, scope);
|
|
932
928
|
// Credential declarations own their readiness even when a static catalog
|
|
933
929
|
// or a plugin status would otherwise report success. Never probe an empty slot.
|
|
934
930
|
if (connector.credential) {
|
|
@@ -1108,6 +1104,10 @@ class ScopedRegistryView {
|
|
|
1108
1104
|
? registry.statusFor(args[0], args[1], args[2], args[3], this.scope)
|
|
1109
1105
|
: Promise.resolve({ state: "error", message: "Unknown connector" });
|
|
1110
1106
|
}
|
|
1107
|
+
withCatalogObservations(id, baseUrl, status) {
|
|
1108
|
+
return (this.registryFor(id)?.withCatalogObservations(id, baseUrl, status, this.scope) ??
|
|
1109
|
+
ownStatus({ state: "error", message: "Unknown connector" }));
|
|
1110
|
+
}
|
|
1111
1111
|
invalidateStored(id) {
|
|
1112
1112
|
const registry = this.registryFor(id);
|
|
1113
1113
|
return registry ? registry.invalidateStored(id) : Promise.resolve();
|
package/dist/routes/ui-data.js
CHANGED
|
@@ -74,7 +74,13 @@ export function uiData(registry, baseUrl, options) {
|
|
|
74
74
|
}))).pipe(Effect.map((tools) => ({ status, tools, catalogFailed: false })),
|
|
75
75
|
// The registry owns the failed catalog observation; the page only
|
|
76
76
|
// needs to know there was one.
|
|
77
|
-
Effect.catch(() => Effect.succeed({ status, tools: [], catalogFailed: !signal.aborted }))
|
|
77
|
+
Effect.catch(() => Effect.succeed({ status, tools: [], catalogFailed: !signal.aborted })),
|
|
78
|
+
// Loading the catalog may replace an earlier clean observation.
|
|
79
|
+
// Read this view's current bounded facts without another status probe.
|
|
80
|
+
Effect.map((observed) => ({
|
|
81
|
+
...observed,
|
|
82
|
+
status: registry.withCatalogObservations(c.id, baseUrl, observed.status),
|
|
83
|
+
})));
|
|
78
84
|
}).pipe(Effect.catch((error) => Effect.succeed({
|
|
79
85
|
status: failureStatus(c.id, error),
|
|
80
86
|
tools: [],
|
|
@@ -168,7 +174,7 @@ export function uiData(registry, baseUrl, options) {
|
|
|
168
174
|
toolCount: tools.length,
|
|
169
175
|
tools,
|
|
170
176
|
// Counts only, and only when a refresh in this runtime produced them.
|
|
171
|
-
// `Registry.
|
|
177
|
+
// `Registry.withCatalogObservations` rebuilt the report through
|
|
172
178
|
// `boundedCatalogDrift`, so what lands here cannot carry a name or a
|
|
173
179
|
// schema even if the plugin seam returned one.
|
|
174
180
|
...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
|
package/dist/skills.js
CHANGED
|
@@ -86,7 +86,11 @@ export function connectorSkillName(connectorId) {
|
|
|
86
86
|
export function connectorGuide(connector) {
|
|
87
87
|
const guide = connector.usageGuide;
|
|
88
88
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
89
|
-
|
|
89
|
+
if (!content || content.trim() === "")
|
|
90
|
+
return undefined;
|
|
91
|
+
return connector.readOnly === true
|
|
92
|
+
? `Read-only connection: only read-classified tools are available. Write calls are refused, including tools named in this guide.\n\n${content}`
|
|
93
|
+
: content;
|
|
90
94
|
}
|
|
91
95
|
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
92
96
|
export const GUIDE_SUMMARY_LENGTH = 120;
|
|
@@ -298,7 +302,9 @@ export function connectorGuideSummary(connector) {
|
|
|
298
302
|
// normalization here so direct Connector callers see the same text.
|
|
299
303
|
boundedSummary(connector.usageGuide.summary ?? "")
|
|
300
304
|
: undefined;
|
|
301
|
-
|
|
305
|
+
const original = typeof connector.usageGuide === "string" ? connector.usageGuide : connector.usageGuide?.content;
|
|
306
|
+
const summary = configured ?? summarizeGuide(connector, original ?? guide);
|
|
307
|
+
return connector.readOnly === true ? boundedSummary(`Read-only. ${summary}`) : summary;
|
|
302
308
|
}
|
|
303
309
|
/** Whether correct use always depends on conventions outside the tool schema. */
|
|
304
310
|
export function connectorGuideRequired(connector) {
|
package/dist/types.d.ts
CHANGED
|
@@ -252,12 +252,15 @@ export interface ReviewedTool {
|
|
|
252
252
|
/** Why the verdict holds when the name or downstream annotations do not say. */
|
|
253
253
|
readonly reason?: string;
|
|
254
254
|
/**
|
|
255
|
-
* `sha256:<hex>` digest of the input
|
|
255
|
+
* `sha256-sets-v1:<hex>` digest of the reviewed input/output schemas.
|
|
256
|
+
* Legacy `sha256:<hex>` pins retain literal array ordering.
|
|
256
257
|
* Omit until a review has actually read them; an invented digest reports
|
|
257
258
|
* drift that never happened. When the live schema no longer matches it, or
|
|
258
259
|
* it cannot be checked, the verdict lapses and the tool is served as a write.
|
|
259
260
|
*/
|
|
260
261
|
readonly schemaDigest?: string;
|
|
262
|
+
/** Absence is expected under plan, feature, or permission gating. */
|
|
263
|
+
readonly optional?: boolean;
|
|
261
264
|
}
|
|
262
265
|
/**
|
|
263
266
|
* A reviewed classification of a downstream MCP catalog, keyed by exact tool
|
|
@@ -287,7 +290,7 @@ export interface CatalogDriftCounts {
|
|
|
287
290
|
droppedTools?: number;
|
|
288
291
|
/** Live tools no release classified. Each one fails closed at call time. */
|
|
289
292
|
unclassifiedTools: number;
|
|
290
|
-
/**
|
|
293
|
+
/** Required reviewed names this catalog no longer serves. */
|
|
291
294
|
unservedTools: number;
|
|
292
295
|
/** Explicit downstream annotations that contradict a vetted verdict. */
|
|
293
296
|
annotationConflicts: number;
|
|
@@ -296,6 +299,15 @@ export interface CatalogDriftCounts {
|
|
|
296
299
|
}
|
|
297
300
|
/** One drift observation, taken while serving a catalog refresh. */
|
|
298
301
|
export interface CatalogDriftReport extends CatalogDriftCounts {
|
|
302
|
+
/** False when this listing has no reviewed classification to compare. */
|
|
303
|
+
reviewedBaseline?: boolean;
|
|
304
|
+
/** Whether all pinned schemas could be checked on this observation. */
|
|
305
|
+
verification?: "complete" | "unavailable";
|
|
306
|
+
/** Live reviewed tools with and without recorded schema digests. */
|
|
307
|
+
schemaCoverage?: {
|
|
308
|
+
pinned: number;
|
|
309
|
+
unpinned: number;
|
|
310
|
+
};
|
|
299
311
|
/** When the observation was taken; never when a probe was scheduled. */
|
|
300
312
|
observedAt: string;
|
|
301
313
|
}
|
|
@@ -364,6 +376,8 @@ export type ConnectorSkillResourceContents = {
|
|
|
364
376
|
});
|
|
365
377
|
/** The whole plugin contract — the one open seam. */
|
|
366
378
|
export interface Connector {
|
|
379
|
+
/** Serve only read-classified tools on every endpoint. Defaults to false. */
|
|
380
|
+
readOnly?: boolean;
|
|
367
381
|
id: string;
|
|
368
382
|
/**
|
|
369
383
|
* Who owns this connector's downstream authentication. `shared` keeps one
|
|
@@ -398,7 +412,7 @@ export interface Connector {
|
|
|
398
412
|
* cross-operation sequencing rule).
|
|
399
413
|
*
|
|
400
414
|
* Served as `skill://connecta/connectors/<slug>/SKILL.md`, with generated
|
|
401
|
-
* frontmatter
|
|
415
|
+
* frontmatter, a read-only policy notice when configured, and the guide body. `connector:<id>` is a
|
|
402
416
|
* one-release lookup alias. The guide remains deployment-owned configuration;
|
|
403
417
|
* no runtime registration or shared mutable copy exists.
|
|
404
418
|
*/
|
|
@@ -605,7 +619,7 @@ export interface ConnectorDescription {
|
|
|
605
619
|
tools?: ConnectorToolDescription[];
|
|
606
620
|
}
|
|
607
621
|
export interface ConnectorUsageGuide {
|
|
608
|
-
/** Markdown
|
|
622
|
+
/** Markdown served by `skills`, with a policy notice when the connector is read-only. */
|
|
609
623
|
content: string;
|
|
610
624
|
/**
|
|
611
625
|
* Discovery hint describing the conventions the guide covers. Whitespace is
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTA_VERSION = "0.34.
|
|
1
|
+
export declare const CONNECTA_VERSION = "0.34.1";
|
package/dist/version.js
CHANGED
|
@@ -148,6 +148,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
148
148
|
(`src/membership.ts`, `admitsMaintainer`, `memberPool`, and `agentPool` in
|
|
149
149
|
`src/routes/shared.ts`). Nothing intersects or widens either view; every
|
|
150
150
|
refusal is one 404 naming no pool.
|
|
151
|
+
Connector `readOnly` is enforced by catalog discovery and invocation before dispatch: effective read-only is endpoint OR connector, including on plain `/mcp`. Maintained provider construction carries the policy through the shared factory.
|
|
151
152
|
The endpoint's `readOnly` is the pool's own value, else the deployment's;
|
|
152
153
|
plain `/mcp` uses the deployment's. It travels to the meta-tools and the
|
|
153
154
|
program as one boolean that drops writes from `CatalogService` search and
|
|
@@ -158,7 +159,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
158
159
|
narrower slice, and the subject and principal keys. Personal connectors use
|
|
159
160
|
the principal partition and result paging the subject partition with principal,
|
|
160
161
|
endpoint/pool, origin, connector, and tool bindings. Paging re-admits the
|
|
161
|
-
caller at its endpoint and rechecks
|
|
162
|
+
caller at its endpoint and rechecks endpoint OR connector `readOnly`; no caller parameter selects a partition (`test/identity-scope.test.ts`).
|
|
162
163
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
163
164
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
164
165
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
@@ -1062,7 +1063,7 @@ every vendor ships one reviewed table through one mechanism
|
|
|
1062
1063
|
message/error and raw string/object source payloads in both events and
|
|
1063
1064
|
invocation maps. The shared review harness rejects embedded-URL operation
|
|
1064
1065
|
keys absent from each vendor's pinned index and invalid path syntax.
|
|
1065
|
-
Vercel
|
|
1066
|
+
The current Vercel pin was reviewed from its full response document; its unversioned URL does not retain older documents.
|
|
1066
1067
|
Generic parameters in descriptions, metadata, nicknames, billing footers,
|
|
1067
1068
|
notes, DNS TXT, KV values and operator HTML retain their original bytes,
|
|
1068
1069
|
including whole URL strings. Ordinary code/key URLs can be false positives
|
|
@@ -1165,7 +1166,7 @@ a table.
|
|
|
1165
1166
|
|
|
1166
1167
|
`remoteMcp({ classify })` is the public way to declare what a downstream's
|
|
1167
1168
|
tools do: `{ tools: { name: "read" | "write" | "destructive" | { verdict,
|
|
1168
|
-
reason?, schemaDigest? } }, unlisted?: "hide" }`, validated at construction. It fails closed
|
|
1169
|
+
reason?, schemaDigest?, optional? } }, unlisted?: "hide" }`, validated at construction. It fails closed
|
|
1169
1170
|
(INV-1): a reviewed read fills silence but yields to an explicit write
|
|
1170
1171
|
annotation, a reviewed write stays a write whatever the downstream claims, an
|
|
1171
1172
|
unlisted tool is a read only when it says so, and a reviewed tool whose
|
|
@@ -1191,8 +1192,50 @@ receives a separate deep copy of its definition, so mutations cannot change
|
|
|
1191
1192
|
classification, schemas, or write accounting later in the same program.
|
|
1192
1193
|
A restart applies the current review to cached SDK facts. Retired registry
|
|
1193
1194
|
manifest versions 2 and 3 are ignored and expire on their original TTL.
|
|
1194
|
-
During
|
|
1195
|
-
|
|
1195
|
+
During listings the registry counts drift against the same record. Optional
|
|
1196
|
+
reviewed tools do not count as unserved when gated out. Reports carry an explicit
|
|
1197
|
+
reviewed-baseline fact, counts of live reviewed tools with and without schema
|
|
1198
|
+
pins, and complete/unavailable schema verification. A digest failure replaces
|
|
1199
|
+
the previous observation rather than retaining a stale clean result (INV-1,
|
|
1200
|
+
INV-6). Observation and classification share the same verification result for each
|
|
1201
|
+
listing; classification still fails closed.
|
|
1202
|
+
|
|
1203
|
+
Reports are in-memory only, weakly keyed by connector and a registry-owned
|
|
1204
|
+
catalog partition token. The partition uses the same admitted principal,
|
|
1205
|
+
subject, caller identity, authenticated flag and pool as catalog caching, plus
|
|
1206
|
+
connector id and endpoint URLs. Personal registries do not share tokens with
|
|
1207
|
+
each other or the root. Each registry retains at most 1,024 tokens; eviction or
|
|
1208
|
+
restart yields no observation, never another subject's counts. No new storage
|
|
1209
|
+
key or durable catalog record is introduced.
|
|
1210
|
+
|
|
1211
|
+
New schema pins use `sha256-sets-v1:<hex>`. Canonicalization sorts object keys
|
|
1212
|
+
and schema `required`, `enum`, and `type` arrays, including schema-valued
|
|
1213
|
+
legacy dependencies. Tuple arrays and JSON data
|
|
1214
|
+
inside defaults, constants, enum members and examples retain order. Existing
|
|
1215
|
+
`sha256:<hex>` pins continue using the original key-sorted, literal-array
|
|
1216
|
+
algorithm; their reviewed digests are unchanged. Migrating a legacy pin requires
|
|
1217
|
+
reviewing the complete schemas and recording a new versioned digest. Legacy
|
|
1218
|
+
pins may still report conservative drift for array reordering until migrated.
|
|
1219
|
+
The iterative digest retains the whole-schema node ceiling in both formats.
|
|
1220
|
+
|
|
1221
|
+
Contributor evidence: `test/provider-schema-pins.test.ts` checks every provider
|
|
1222
|
+
construction case in the generated roster. Every pinned digest requires a
|
|
1223
|
+
complete captured schema that passes runtime verification; changed input or
|
|
1224
|
+
output schemas must fail it. The guard also exercises both digest prefixes.
|
|
1225
|
+
|
|
1226
|
+
The maintainer `scripts/drift-check.mjs` compares published inventories with
|
|
1227
|
+
provider evidence. Table inventories can require `inventory.accessColumn` and
|
|
1228
|
+
record `reviewedAccess` (`read` or `write`, with destructive verdicts recorded
|
|
1229
|
+
as write). These access snapshots must agree with the provider's runtime
|
|
1230
|
+
classification. A documented access change or disappearance of a reviewed explicit Access
|
|
1231
|
+
value is advisory drift, never an
|
|
1232
|
+
automatically accepted verdict. An explicit access value makes an
|
|
1233
|
+
`acknowledgedUnclassified` entry stale: report it and resume reporting that
|
|
1234
|
+
unreviewed tool as an addition. Missing tools and already reviewed names also
|
|
1235
|
+
invalidate such acknowledgements. Duplicate explicit access values must agree;
|
|
1236
|
+
a blank duplicate does not erase an explicit value elsewhere. Missing required
|
|
1237
|
+
columns in any tool table, unknown access values and conflicting rows are provider-local parser
|
|
1238
|
+
findings. The internal `withVettedCatalog()`
|
|
1196
1239
|
helper has been removed. Public provider `*_VETTED_CATALOG` exports remain
|
|
1197
1240
|
deprecated aliases derived from each definition
|
|
1198
1241
|
(`test/classified-decorators.test.ts`, `test/hosted-presets.test.ts`).
|