@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
|
@@ -3,8 +3,8 @@ export const skill = {
|
|
|
3
3
|
"content": "- 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",
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
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- `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",
|
|
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, 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
|
@@ -16,6 +16,8 @@ import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
|
|
|
16
16
|
import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
|
|
17
17
|
import { attachOAuthPartition, oauthPartitionIdle } from "./oauth-partition.js";
|
|
18
18
|
import { attachCaller } from "./connector-caller.js";
|
|
19
|
+
import { isWorkspaceAddress } from "./workspace-address.js";
|
|
20
|
+
import { workspaceSubjectOf } from "./workspace-subject.js";
|
|
19
21
|
import { runEdge } from "./runtime/run.js";
|
|
20
22
|
import { SharedRead } from "./runtime/shared-read.js";
|
|
21
23
|
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet } from "./runtime/storage.js";
|
|
@@ -227,13 +229,8 @@ export class Registry {
|
|
|
227
229
|
catalogObservedAt = new Map();
|
|
228
230
|
/** Last payload-free agent catalog access in this runtime. */
|
|
229
231
|
catalogAccess = new Map();
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Schema digest verification per reviewed facts array. Only the registry's
|
|
234
|
-
* own deep-frozen arrays are keys, so an entry stays true while they live.
|
|
235
|
-
*/
|
|
236
|
-
verifiedFacts = new WeakMap();
|
|
232
|
+
/** Bounded tokens isolating count-only observations by catalog identity. */
|
|
233
|
+
catalogObservationPartitions = new Map();
|
|
237
234
|
observedOutputSchemas;
|
|
238
235
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
239
236
|
maxResultBytes;
|
|
@@ -261,6 +258,9 @@ export class Registry {
|
|
|
261
258
|
if (this.connectors.has(c.id)) {
|
|
262
259
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
263
260
|
}
|
|
261
|
+
if (c.readOnly !== undefined && typeof c.readOnly !== "boolean") {
|
|
262
|
+
throw new Error(`Connector "${c.id}" readOnly must be a boolean.`);
|
|
263
|
+
}
|
|
264
264
|
if (c.authScope !== undefined && c.authScope !== "shared" && c.authScope !== "personal") {
|
|
265
265
|
throw new Error(`Invalid authScope on connector "${c.id}": expected "shared" or "personal"`);
|
|
266
266
|
}
|
|
@@ -330,9 +330,6 @@ export class Registry {
|
|
|
330
330
|
catalogStorage: this.opts.catalogStorage ?? this.opts.storage,
|
|
331
331
|
constructionChecks: false,
|
|
332
332
|
});
|
|
333
|
-
// Counts only, like the connector-wide reviewed drift observation. No
|
|
334
|
-
// principal, credentials, or catalog payload crosses this registry boundary.
|
|
335
|
-
registry.droppedToolNames = this.droppedToolNames;
|
|
336
333
|
if (this.callAdmissionClosed)
|
|
337
334
|
registry.closeCallAdmission();
|
|
338
335
|
this.personalRegistries.set(principalKey, registry);
|
|
@@ -501,13 +498,7 @@ export class Registry {
|
|
|
501
498
|
attachCaller(context, scope?.caller);
|
|
502
499
|
attachCatalogCache(context, {
|
|
503
500
|
storage: this.opts.catalogStorage ?? this.opts.storage,
|
|
504
|
-
partition:
|
|
505
|
-
scope?.principalKey ?? this.opts.credentialOwner ?? null,
|
|
506
|
-
scope?.subjectKey ?? null,
|
|
507
|
-
scope?.caller?.identity ?? null,
|
|
508
|
-
scope?.caller?.authenticated ?? false,
|
|
509
|
-
scope?.caller?.pool ?? null,
|
|
510
|
-
]),
|
|
501
|
+
partition: this.catalogPartition(scope),
|
|
511
502
|
sharedPartition: JSON.stringify([baseUrl, this.opts.publicUrl ?? null, scope?.caller?.pool ?? null]),
|
|
512
503
|
defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
|
|
513
504
|
minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
|
|
@@ -551,26 +542,38 @@ export class Registry {
|
|
|
551
542
|
}
|
|
552
543
|
return Object.fromEntries([...snapshots].map(([id, values]) => [id, aggregateCallAdmissionSnapshots(values)]));
|
|
553
544
|
}
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
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));
|
|
574
577
|
}
|
|
575
578
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
576
579
|
closeCallAdmission() {
|
|
@@ -743,7 +746,7 @@ export class Registry {
|
|
|
743
746
|
if (!connector)
|
|
744
747
|
throw new Error(`Unknown connector "${id}"`);
|
|
745
748
|
if (connector.staticTools)
|
|
746
|
-
return markCatalogFreshness(connector.staticTools, Infinity);
|
|
749
|
+
return { tools: markCatalogFreshness(connector.staticTools, Infinity), verification: undefined };
|
|
747
750
|
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
748
751
|
let listed;
|
|
749
752
|
try {
|
|
@@ -764,10 +767,8 @@ export class Registry {
|
|
|
764
767
|
}
|
|
765
768
|
const facts = frozenFacts(structuredClone(tools));
|
|
766
769
|
const accepted = this.acceptToolNames(facts);
|
|
767
|
-
this.observeDroppedToolNames(connector, facts.length - accepted.length);
|
|
768
770
|
const review = catalogReviewOf(connector);
|
|
769
|
-
|
|
770
|
-
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);
|
|
771
772
|
if (catalogFetchedAt(ctx) === undefined) {
|
|
772
773
|
try {
|
|
773
774
|
await storeCustomCatalogFallback(ctx, id, facts);
|
|
@@ -789,19 +790,13 @@ export class Registry {
|
|
|
789
790
|
state: freshUntil > Date.now() ? "fresh" : "stale",
|
|
790
791
|
observedAt: new Date().toISOString(),
|
|
791
792
|
});
|
|
792
|
-
return markCatalogFreshness(
|
|
793
|
+
return { tools: markCatalogFreshness(accepted, freshUntil), verification };
|
|
793
794
|
}
|
|
794
795
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
795
796
|
acceptToolNames(tools) {
|
|
796
797
|
const accepted = tools.filter((tool) => !hasControlCharacters(tool.name));
|
|
797
798
|
return accepted.length === tools.length ? tools : accepted;
|
|
798
799
|
}
|
|
799
|
-
/** Observe intake once, never while serving an older cached listing. */
|
|
800
|
-
observeDroppedToolNames(connector, count) {
|
|
801
|
-
if (count > 0 || this.droppedToolNames.has(connector.id)) {
|
|
802
|
-
this.droppedToolNames.set(connector.id, { count, observedAt: new Date().toISOString() });
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
800
|
/**
|
|
806
801
|
* One connector's catalog as served: the cached downstream facts, classified
|
|
807
802
|
* on every read by the connector's `classification`, into fresh objects.
|
|
@@ -811,7 +806,7 @@ export class Registry {
|
|
|
811
806
|
* the next read.
|
|
812
807
|
*/
|
|
813
808
|
async loadTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
814
|
-
const tools = await this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
|
|
809
|
+
const { tools, verification } = await this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
|
|
815
810
|
const connector = this.connectors.get(id);
|
|
816
811
|
if (!connector)
|
|
817
812
|
throw new Error(`Unknown connector "${id}"`);
|
|
@@ -820,7 +815,7 @@ export class Registry {
|
|
|
820
815
|
this.validateClassification(id, accepted, overrides);
|
|
821
816
|
const review = catalogReviewOf(connector);
|
|
822
817
|
return carryCatalogFreshness(tools, review
|
|
823
|
-
? await classifyCatalog(review, id, accepted, this.opts.logger,
|
|
818
|
+
? await classifyCatalog(review, id, accepted, this.opts.logger, overrides, verification)
|
|
824
819
|
: accepted.map((tool) => this.publishUnreviewedTool(id, tool)));
|
|
825
820
|
}
|
|
826
821
|
publishUnreviewedTool(id, tool) {
|
|
@@ -902,6 +897,20 @@ export class Registry {
|
|
|
902
897
|
const observedAt = this.catalogObservedAt.get(id);
|
|
903
898
|
return observedAt === undefined ? null : Math.max(0, Date.now() - observedAt);
|
|
904
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
|
+
}
|
|
905
914
|
/** Best-effort connector status for the operator UI. */
|
|
906
915
|
async statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
907
916
|
const connector = this.connectors.get(id);
|
|
@@ -915,18 +924,7 @@ export class Registry {
|
|
|
915
924
|
// The report is rebuilt rather than spread through: what the seam returned
|
|
916
925
|
// is third-party output, and status is read by the operator UI and copied
|
|
917
926
|
// into responses.
|
|
918
|
-
const withObservations = (status) =>
|
|
919
|
-
const report = this.catalogDriftOf(connector);
|
|
920
|
-
const access = this.catalogAccess.get(id);
|
|
921
|
-
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
922
|
-
// a connector cannot smuggle payload through either registry-owned
|
|
923
|
-
// observation when this runtime has not made one, nor through its
|
|
924
|
-
// message, which survives only when connecta wrote it (INV-6).
|
|
925
|
-
return Object.assign(boundedStatus(status), {
|
|
926
|
-
...(report ? { catalogDrift: report } : {}),
|
|
927
|
-
...(access ? { catalogAccess: { ...access } } : {}),
|
|
928
|
-
});
|
|
929
|
-
};
|
|
927
|
+
const withObservations = (status) => this.withCatalogObservations(id, baseUrl, status, scope);
|
|
930
928
|
// Credential declarations own their readiness even when a static catalog
|
|
931
929
|
// or a plugin status would otherwise report success. Never probe an empty slot.
|
|
932
930
|
if (connector.credential) {
|
|
@@ -997,10 +995,23 @@ class ScopedRegistryView {
|
|
|
997
995
|
const connector = this.root.getConnector(id);
|
|
998
996
|
if (!connector)
|
|
999
997
|
return undefined;
|
|
998
|
+
// An agent has no Workspace account: on a mapping-subject Workspace
|
|
999
|
+
// connector it acts as the pool's owner. Without one there is no account
|
|
1000
|
+
// to act as, so the connector is absent, as a personal connector is.
|
|
1001
|
+
if (this.agentWithoutOwner && workspaceSubjectOf(connector)?.kind === "mapping")
|
|
1002
|
+
return undefined;
|
|
1000
1003
|
if (connector.authScope !== "personal")
|
|
1001
1004
|
return this.root;
|
|
1002
1005
|
return admission ? this.admissionPersonal : this.personal;
|
|
1003
1006
|
}
|
|
1007
|
+
/** An agent admitted through a pool that names no Workspace owner. */
|
|
1008
|
+
get agentWithoutOwner() {
|
|
1009
|
+
const caller = this.scope.caller;
|
|
1010
|
+
if (caller?.identity.agent === undefined)
|
|
1011
|
+
return false;
|
|
1012
|
+
// The same test the Workspace provider applies before acting as the owner.
|
|
1013
|
+
return caller.pool === undefined || !isWorkspaceAddress(caller.poolOwner);
|
|
1014
|
+
}
|
|
1004
1015
|
listConnectors() {
|
|
1005
1016
|
return this.root.listConnectors().filter((connector) => this.registryFor(connector.id) !== undefined);
|
|
1006
1017
|
}
|
|
@@ -1093,6 +1104,10 @@ class ScopedRegistryView {
|
|
|
1093
1104
|
? registry.statusFor(args[0], args[1], args[2], args[3], this.scope)
|
|
1094
1105
|
: Promise.resolve({ state: "error", message: "Unknown connector" });
|
|
1095
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
|
+
}
|
|
1096
1111
|
invalidateStored(id) {
|
|
1097
1112
|
const registry = this.registryFor(id);
|
|
1098
1113
|
return registry ? registry.invalidateStored(id) : Promise.resolve();
|
package/dist/routes/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Duration, Effect, Exit, Option, Result, Scope } from "effect";
|
|
|
8
8
|
import { registerExecuteTool } from "../execute.js";
|
|
9
9
|
import { ExecutorAdmissionError } from "../executor-admission.js";
|
|
10
10
|
import { registerMetaTools } from "../meta-tools.js";
|
|
11
|
-
import {
|
|
11
|
+
import { endpointInstructions } from "../skills.js";
|
|
12
12
|
import { registerSkills } from "./skills.js";
|
|
13
13
|
import { failureRecord, logFailure } from "../operator-record.js";
|
|
14
14
|
import { redactAgentOutput, sentSecretsForRequest } from "../sent-secrets.js";
|
|
@@ -341,7 +341,10 @@ function serveMcp(request, requestSignal, requestScope, opts, baseUrl, actor, re
|
|
|
341
341
|
// A request-local server cannot publish catalog changes. Set this before
|
|
342
342
|
// tool registration, whose SDK default otherwise advertises listChanged.
|
|
343
343
|
capabilities: { tools: { listChanged: false }, extensions: { "io.modelcontextprotocol/skills": {} } },
|
|
344
|
-
|
|
344
|
+
// Instructions differ per endpoint only by the admitted pool's
|
|
345
|
+
// description. Every cache hint below is private, so a client keeps
|
|
346
|
+
// what it learned per endpoint URL and never shares it across them.
|
|
347
|
+
instructions: endpointInstructions(pool !== undefined ? opts.pools.get(pool)?.description : undefined),
|
|
345
348
|
inputRequired: { legacyShim: false },
|
|
346
349
|
requestState: { verify: (state, context) => authElicitation.verify(state, context) },
|
|
347
350
|
cacheHints: {
|
|
@@ -738,6 +741,18 @@ export function createMcpRoute(opts) {
|
|
|
738
741
|
return yield* refuseAdmission(late.failure);
|
|
739
742
|
}
|
|
740
743
|
}
|
|
744
|
+
// The caller rides on the view, and the paging recheck builds the
|
|
745
|
+
// same one: an agent's Workspace visibility depends on the owner.
|
|
746
|
+
const callerFor = (identity, owner) => ({
|
|
747
|
+
identity,
|
|
748
|
+
// `authorize` admits an open deployment's every request as the
|
|
749
|
+
// anonymous actor; only a provider's `ok` is an authentication.
|
|
750
|
+
authenticated: opts.config.auth.length > 0,
|
|
751
|
+
// Both pool endpoints build the caller here from the same
|
|
752
|
+
// admitted scope, so an agent at /agent/<pool> carries the owner.
|
|
753
|
+
...(poolName !== undefined ? { pool: poolName } : {}),
|
|
754
|
+
...(owner !== undefined ? { poolOwner: owner } : {}),
|
|
755
|
+
});
|
|
741
756
|
let scopedRegistry;
|
|
742
757
|
try {
|
|
743
758
|
scopedRegistry = opts.registry.scoped({
|
|
@@ -760,11 +775,12 @@ export function createMcpRoute(opts) {
|
|
|
760
775
|
const currentScope = scopeFor(current);
|
|
761
776
|
if (!currentScope)
|
|
762
777
|
return false;
|
|
763
|
-
const { access: currentAccess, readOnly: currentReadOnly } = currentScope;
|
|
778
|
+
const { access: currentAccess, readOnly: currentReadOnly, poolOwner: currentOwner } = currentScope;
|
|
764
779
|
const view = opts.registry.scoped({
|
|
765
780
|
...currentAccess,
|
|
766
781
|
...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
|
|
767
782
|
...(current.principalKey ? { principalKey: current.principalKey } : {}),
|
|
783
|
+
caller: callerFor(current.identity, currentOwner),
|
|
768
784
|
});
|
|
769
785
|
if (classification === "write" && currentReadOnly)
|
|
770
786
|
return false;
|
|
@@ -789,16 +805,7 @@ export function createMcpRoute(opts) {
|
|
|
789
805
|
await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
|
|
790
806
|
}
|
|
791
807
|
},
|
|
792
|
-
caller:
|
|
793
|
-
identity: authz.identity,
|
|
794
|
-
// `authorize` admits an open deployment's every request as the
|
|
795
|
-
// anonymous actor; only a provider's `ok` is an authentication.
|
|
796
|
-
authenticated: opts.config.auth.length > 0,
|
|
797
|
-
// Both pool endpoints build the caller here from the same
|
|
798
|
-
// admitted scope, so an agent at /agent/<pool> carries the owner.
|
|
799
|
-
...(poolName !== undefined ? { pool: poolName } : {}),
|
|
800
|
-
...(poolOwner !== undefined ? { poolOwner } : {}),
|
|
801
|
-
},
|
|
808
|
+
caller: callerFor(authz.identity, poolOwner),
|
|
802
809
|
});
|
|
803
810
|
}
|
|
804
811
|
catch {
|
package/dist/routes/oauth.js
CHANGED
|
@@ -38,7 +38,7 @@ function html(reason, opts, connector) {
|
|
|
38
38
|
const details = outcome.fixPrompt
|
|
39
39
|
? `<details class="status-details">
|
|
40
40
|
<summary>Details for the operator</summary>
|
|
41
|
-
<p>If this keeps happening, send this to whoever runs ${escapeHtml(brand.productName)}. It is
|
|
41
|
+
<p>If this keeps happening, send this to whoever runs ${escapeHtml(brand.productName)}. It is for a coding agent and holds nothing from the provider.</p>
|
|
42
42
|
<pre>${escapeHtml(outcome.fixPrompt)}</pre>
|
|
43
43
|
</details>`
|
|
44
44
|
: "";
|
|
@@ -55,7 +55,7 @@ function html(reason, opts, connector) {
|
|
|
55
55
|
</section>
|
|
56
56
|
</main>`;
|
|
57
57
|
return new Response(renderPage(opts.config.ui?.branding, {
|
|
58
|
-
title: `${outcome.heading}
|
|
58
|
+
title: `${outcome.heading} · ${brand.pageTitle}`,
|
|
59
59
|
uiMounted,
|
|
60
60
|
body,
|
|
61
61
|
}), { status: outcome.status, headers: htmlSecurityHeaders({ "Content-Type": "text/html; charset=utf-8" }) });
|
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
|
@@ -13,6 +13,16 @@ const RECOVERY = 'When an auth error includes a handoff, give it to the user or
|
|
|
13
13
|
* and describe and refuses every write call. Guides may still name them.
|
|
14
14
|
*/
|
|
15
15
|
export const CONNECTA_INSTRUCTIONS = `${ROUTE} On a writable endpoint, programs and call_destructive_tool may write. On a read-only endpoint, search omits write tools and every write call is refused; guides may still name them. The execute_code description says which this endpoint is. Never repeat a write to recover its output. ${RECOVERY}`;
|
|
16
|
+
/**
|
|
17
|
+
* The instructions one endpoint serves: {@link CONNECTA_INSTRUCTIONS}, then
|
|
18
|
+
* the admitted pool's `description` after a fixed lead-in. Plain `/mcp` and a
|
|
19
|
+
* pool without a description serve the constant unchanged. The description
|
|
20
|
+
* comes from deployment config for the pool the request was admitted through;
|
|
21
|
+
* nothing a request sends reaches it.
|
|
22
|
+
*/
|
|
23
|
+
export function endpointInstructions(description) {
|
|
24
|
+
return description === undefined ? CONNECTA_INSTRUCTIONS : `${CONNECTA_INSTRUCTIONS} This endpoint: ${description}`;
|
|
25
|
+
}
|
|
16
26
|
export { USAGE_SKILL } from "./usage-guide.js";
|
|
17
27
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
18
28
|
export function hasConnectorGuides(connectors) {
|
|
@@ -76,7 +86,11 @@ export function connectorSkillName(connectorId) {
|
|
|
76
86
|
export function connectorGuide(connector) {
|
|
77
87
|
const guide = connector.usageGuide;
|
|
78
88
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
79
|
-
|
|
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;
|
|
80
94
|
}
|
|
81
95
|
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
82
96
|
export const GUIDE_SUMMARY_LENGTH = 120;
|
|
@@ -288,7 +302,9 @@ export function connectorGuideSummary(connector) {
|
|
|
288
302
|
// normalization here so direct Connector callers see the same text.
|
|
289
303
|
boundedSummary(connector.usageGuide.summary ?? "")
|
|
290
304
|
: undefined;
|
|
291
|
-
|
|
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;
|
|
292
308
|
}
|
|
293
309
|
/** Whether correct use always depends on conventions outside the tool schema. */
|
|
294
310
|
export function connectorGuideRequired(connector) {
|
package/dist/storage/sql.js
CHANGED
|
@@ -492,7 +492,11 @@ export function sqlActivityStore(driver, kind, options = {}) {
|
|
|
492
492
|
}
|
|
493
493
|
});
|
|
494
494
|
return {
|
|
495
|
-
describe: () => ({
|
|
495
|
+
describe: () => ({
|
|
496
|
+
kind,
|
|
497
|
+
retentionDays,
|
|
498
|
+
optionSources: { retentionDays: options.retentionDays === undefined ? "default" : "config" },
|
|
499
|
+
}),
|
|
496
500
|
async record(event) {
|
|
497
501
|
await ensure();
|
|
498
502
|
const occurredAtMs = Date.parse(event.occurredAt);
|