@zackbart/connecta 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +15 -7
- package/bin/connecta.mjs +24 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/config-value-sources.js +11 -9
- package/dist/config.d.ts +37 -75
- package/dist/config.js +110 -36
- package/dist/connector-access.d.ts +4 -34
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +29 -7
- package/dist/describe-config.js +7 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +29 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +82 -21
- package/dist/page-styles.js +1 -1
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +30 -8
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +3 -3
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +3 -3
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +3 -3
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +10 -8
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +6 -3
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +145 -95
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +24 -23
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +5 -0
- package/dist/storage/keys.js +9 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +16 -14
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +355 -50
- package/documentation/auth.md +775 -243
- package/documentation/code-mode.md +14 -11
- package/documentation/deploying.md +121 -24
- package/documentation/integrating.md +92 -21
- package/documentation/meta-tools.md +14 -9
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +115 -28
- package/documentation/operator-ui.md +326 -61
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +30 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +36 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +3 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +27 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +67 -42
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -4,7 +4,7 @@ export const skill = {
|
|
|
4
4
|
"instructionsHeading": "Account instructions",
|
|
5
5
|
"fragments": {
|
|
6
6
|
"oauth": "\n\nThis OAuth session may expose both live and sandbox Stripe accounts. Discover the live account-selection tools, select the intended account and mode, then carry exact context fields such as `stripe_context` and `livemode` wherever the schema requires them. A live-mode write moves real money; a sandbox write changes test data. Never infer the account or mode from connector metadata.\n\n- Discover account and mode with the live account-selection tools before every account-scoped read or write. The published inventory has changed account-tool names; do not assume `list_available_accounts_or_orgs` is available. Select the intended result and carry the exact context fields required by the live schema unchanged. If the account, mode, or supported selector is ambiguous, stop and ask; never guess.\n- Organization accounts are not Stripe Connect connected accounts. A Connect call requires a separate API-key connector whose `connectedAccount` sends Stripe's documented `Stripe-Account` header; OAuth does not support that path.\n- Four generic tools reach any Stripe API method. Find the method with `stripe_api_search`, read its parameters with `stripe_api_details`, then call `stripe_api_read` (GET) or `stripe_api_write` (POST/PATCH/PUT/DELETE). Never guess a path or a parameter name — `stripe_api_details` is cheaper than a rejected write.\n- Prefer a dedicated tool when one covers the task: `get_balance_summary` for balances and `stripe_analytics` for Sigma or Metrics reporting. Use `stripe_api_search` for everything else instead of assuming a retired dedicated tool still exists.\n- `stripe_implementation_planner` and the query-execution intents of `stripe_analytics` create provider-side planning or query-run state. Connecta therefore classifies both as writes, which a read-only endpoint refuses; their retrieval paths stay behind the same tool boundary.\n- `stripe_api_write` carries the blast radius of the entire write API — every POST, PATCH, PUT, and DELETE, from a customer edit to a subscription cancellation. State the method and path explicitly; a read-only endpoint refuses it.\n- Any `stripe_api_read` list or `stripe_api_search` that returns full objects belongs inside `execute_code`, projected to the fields the question needs before `return`. Neither `limit` nor `expand` substitutes for that: an unprojected list of customers or invoices truncates long before it answers, and a projected one keeps the customer's name, email, and address out of the transcript.\n- Find an object with `stripe_api_search` (or a list endpoint through `stripe_api_read`) and carry the `id` it returned into the write.\n- This connection's tool list is not a fixed set. Stripe gates parts of its MCP catalog by account, integration, and beta enrollment, so search this connector for what it actually exposes rather than assuming a documented tool is here.\n- Send an `Idempotency-Key` on every write you might retry, if the tool accepts it, and reuse the same key for the retry. A retry with a fresh key is a second charge, not a second attempt.\n",
|
|
7
|
-
"key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete: substitute ids into the template search returned (`/v1/payment_intents/pi_123`). Pass `query` and `body` as JSON objects named as `stripe_api_details` lists them. Connecta form-encodes v1 bodies and query lists (`expand: [\"latest_charge\"]`), sends v2 bodies as JSON, and pins `Stripe-Version` to the index's API version. Send `null` or `\"\"` to unset a field.\n- Every call is checked against the pinned index before it is sent. An unknown path returns the nearest operations; an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Stripe, 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: `starting_after`, or `page` for search and v2 lists. Lists belong inside `execute_code`; pass `select` (dot paths such as `data.id` and `data.status`) to keep only the fields the question needs, which also keeps customer names, emails, and addresses out of the transcript.\n- `get_stripe_account_info` confirms the account, mode, and connected account; `get_balance_summary` returns available and pending balances. Use them when they answer the question.\n- Find records with list and search operations through `stripe_api_read` (`GET /v1/customers/search`, `GET /v1/charges` with `customer`) and carry the returned `id` into the write.\n- `stripe_api_write` sends an `Idempotency-Key` on every POST. Pass `idempotencyKey` for anything you might retry, or reuse the generated one the result returns. An ambiguous failure carries it in its message or as `uncertainCall.recovery.idempotencyKey`. Reuse it only with the exact original arguments and within Stripe's retention (v1 keys may be pruned after 24 hours); after that, look the object up before retrying. A retry with a fresh key is a second charge, not a second attempt.\n- Refused by design: operations that hand out a credential (ephemeral keys, Terminal connection tokens, account links (v1 and v2), Terminal onboarding links, Express login links, account sessions, customer sessions, billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), Stripe Apps secret payloads (`expand` of `payload`), Issuing card `number` and `cvc` expansion on any Issuing path, multipart uploads such as `POST /v1/files` (use the Dashboard), and the meter event stream (send single events with `POST /v2/billing/meter_events`). Each refusal names its reason. Quote PDFs arrive from `files.stripe.com` as base64.\n- Secrets read as `\"[redacted]\"` in every result, expansions included: client secrets (PaymentIntents, SetupIntents, Checkout and Identity sessions, Sources, Financial Connections, invoice confirmation secrets), webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded request header values, and Issuing card numbers and CVCs. Webhook and event destination URLs, file link URLs (in an expanded File, a list, or an event too), and pre-signed import URLs keep only their scheme and host. Identity verification `url`s come back whole. A Checkout Session's `url` comes back whole only for a guest payment session (`mode: \"payment\"`, no `customer` or `customer_account`, no customer creation, no saved payment method options); any other session returns only its scheme and host, because its page can show, reuse, or remove a customer's saved payment methods. Create those from your own server code, or send a payment link or the invoice's hosted page instead. These rules follow each object wherever it appears, expansions and events included. A `[redacted]` field is never the real value; do not send it back.\n- Failures of credential-bearing operations (webhook endpoints, Apps secrets, Terminal configurations, forwarding) carry Stripe's type, code, decline code, and param, not its message. Payment, billing, and account failures keep Stripe's message.\n- The hosted OAuth connection's natural-language Sigma, metrics, documentation search, implementation planner, and feedback tools do not exist with a key.\n",
|
|
7
|
+
"key": "\n\n- This is Connecta's REST connector to `api.stripe.com`, holding one operator-managed key. It reaches exactly one account in one mode, so there is no account selection and no `stripe_context` or `livemode` argument.\n- `stripe_api_search` finds API operations, not records: it searches the pinned API index and names the tool that calls each operation. `stripe_api_details` returns one operation's parameters with types, enums, and required names. Neither sends a request.\n- `stripe_api_read` calls GET operations, plus `POST /v1/invoices/create_preview`, which only previews. `stripe_api_write` calls every other POST and every DELETE, from a customer edit to a subscription cancellation, with `method` and `path` stated; a read-only endpoint refuses it.\n- Paths are concrete: substitute ids into the template search returned (`/v1/payment_intents/pi_123`). Pass `query` and `body` as JSON objects named as `stripe_api_details` lists them. Connecta form-encodes v1 bodies and query lists (`expand: [\"latest_charge\"]`), sends v2 bodies as JSON, and pins `Stripe-Version` to the index's API version. Send `null` or `\"\"` to unset a field.\n- Every call is checked against the pinned index before it is sent. An unknown path returns the nearest operations; an unknown or missing parameter returns `validation.issues` with the accepted names. Nothing reached Stripe, 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: `starting_after`, or `page` for search and v2 lists. Lists belong inside `execute_code`; pass `select` (dot paths such as `data.id` and `data.status`) to keep only the fields the question needs, which also keeps customer names, emails, and addresses out of the transcript.\n- `get_stripe_account_info` confirms the account, mode, and connected account; `get_balance_summary` returns available and pending balances. Use them when they answer the question.\n- Find records with list and search operations through `stripe_api_read` (`GET /v1/customers/search`, `GET /v1/charges` with `customer`) and carry the returned `id` into the write.\n- `stripe_api_write` sends an `Idempotency-Key` on every POST. Pass `idempotencyKey` for anything you might retry, or reuse the generated one the result returns. An ambiguous failure carries it in its message or as `uncertainCall.recovery.idempotencyKey`. Reuse it only with the exact original arguments and within Stripe's retention (v1 keys may be pruned after 24 hours); after that, look the object up before retrying. A retry with a fresh key is a second charge, not a second attempt.\n- Refused by design: operations that hand out a credential (ephemeral keys, Terminal connection tokens, account links (v1 and v2), Terminal onboarding links, Express login links, account sessions, customer sessions, billing portal sessions, file link creation, Financial Connections session creation, and the meter event session), Stripe Apps secret payloads (`expand` of `payload`), Issuing card `number` and `cvc` expansion on any Issuing path, multipart uploads such as `POST /v1/files` (use the Dashboard), and the meter event stream (send single events with `POST /v2/billing/meter_events`). Each refusal names its reason. Quote PDFs arrive from `files.stripe.com` as base64.\n- Secrets read as `\"[redacted]\"` in every result, expansions included: client secrets (PaymentIntents, SetupIntents, Checkout and Identity sessions, Sources, Financial Connections, invoice confirmation secrets), webhook and event destination signing secrets, Apps secret payloads, app-install authorization codes, Terminal Wi-Fi passwords, forwarded request header values, and Issuing card numbers and CVCs. Webhook and event destination URLs, file link URLs (in an expanded File, a list, or an event too), and pre-signed import URLs keep only their scheme and host. Identity verification `url`s come back whole. A Checkout Session's `url` comes back whole only for a guest payment session (`mode: \"payment\"`, no `customer` or `customer_account`, no customer creation, no saved payment method options); any other session returns only its scheme and host, because its page can show, reuse, or remove a customer's saved payment methods. Create those from your own server code, or send a payment link or the invoice's hosted page instead. These rules follow each object wherever it appears, expansions and events included. A `[redacted]` field is never the real value; do not send it back.\n- Failures of credential-bearing operations (webhook endpoints, Apps secrets, Terminal configurations, forwarding) carry Stripe's type, code, decline code, and param, not its message. Payment, billing, and account failures keep Stripe's message.\n- The hosted OAuth connection's natural-language Sigma, metrics, documentation search, implementation planner, and feedback tools do not exist with a key.\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 charge `failure_message` and `outcome.seller_message` wherever charge objects appear, including event partial copies and vendor error text. Ordinary `code`/`key` links can therefore be false positives in these diagnostics. Descriptions, metadata, price nicknames, invoice footers and notes 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. 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": "- Lists are cursor-paginated: `limit` defaults to 10 and caps at 100, `starting_after` and `ending_before` take an object id and are mutually exclusive, and `has_more` says whether to continue. Page inside `execute_code`.\n- Search filters on a documented per-resource field set, not on arbitrary attributes. Charges search takes `amount`, `created`, `currency`, `customer`, `status`, `refunded`, `disputed`, `metadata`, `billing_details.address.postal_code`, and `payment_method_details.<source>.*` card fields — there is no `payment_intent` field. When the field you want is not searchable, retrieve the parent object and follow its reference (the PaymentIntent's `latest_charge`) instead of retrying the search with another spelling.\n- The account → `stripe_api_search` → `stripe_api_details` → `stripe_api_read` sequence is one program, not four turns: resolve the method once, then call that method as many times as the investigation needs in the same run.\n- Decline outcomes live on the charge — `outcome`, `failure_code`, `failure_message` — reached from the PaymentIntent's `latest_charge`, so \"why did this payment fail\" is a PaymentIntent read followed by one charge read.\n- Resolve ids before acting; never guess one. Stripe ids are typed prefixes — `cus_` customer, `sub_` subscription, `ch_` charge, `pi_` payment intent, `in_` invoice, `acct_` account — and a plausible-looking id belongs to a different object or to nobody.\n- Amounts are integers in the currency's minor unit: `1099` is 10.99 USD, and zero-decimal currencies like JPY take `10` for 10 JPY. Never send a decimal.\n- Treat every create, update, delete, refund, and report run as a write. For guest calls, classification, and routing, fetch `skills({ name: \"usage\" })`.\n",
|
|
9
9
|
"oauth_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of 100 requests per second in live mode and 25 in sandbox mode, and any single endpoint is capped at 25 per second regardless of mode, so paging one list is the real constraint.\n- Use `search_stripe_documentation` when the shape of an object or a flow is unclear; it is a read and costs nothing but a call.\n- An `auth_required` failure means this connector's Stripe authorization is missing or expired: run `authorize_connector` for this connector id, then retry the same call unchanged. A rejected argument or a plan restriction comes back in Stripe's own words instead — read it rather than re-authorizing.\n",
|
|
10
10
|
"key_limits": "- Stripe answers a rate limit with `429` and a `Stripe-Rate-Limited-Reason` header; back off on that rather than retrying immediately. Stripe documents an account ceiling of ",
|
|
@@ -48,6 +48,21 @@
|
|
|
48
48
|
// - Issuing card numbers and CVCs come back only as expansions, which the
|
|
49
49
|
// connector refuses on every Issuing path (`rest.ts`); the card operations
|
|
50
50
|
// also redact both fields.
|
|
51
|
+
// Embedded URL verdict (#812): reviewed paths + global signing parameters.
|
|
52
|
+
// Evidence: digest-verified Stripe v2555, components/schemas/charge's
|
|
53
|
+
// failure_message ("Message to user further explaining reason for charge
|
|
54
|
+
// failure") and charge_outcome.seller_message (Stripe's human-readable
|
|
55
|
+
// outcome/reason). Scan those paths on charge objects, including expansions,
|
|
56
|
+
// events and previous_attributes. All vendor error text is scanned too.
|
|
57
|
+
// Product/customer/charge/invoice descriptions, metadata, price nicknames,
|
|
58
|
+
// invoice footers and notes are customer data: generic code/key/token names
|
|
59
|
+
// there retain their bytes, even in whole URL strings. The required literal
|
|
60
|
+
// X-Amz-/X-Goog- Signature/Credential/Security-Token defense applies globally.
|
|
61
|
+
// file.url/file_link.url and catalog-import upload/download capabilities are
|
|
62
|
+
// structured and retain existing resource/operation rules. The v2555 review
|
|
63
|
+
// found no documented vendor-minted link embedded in other free text; no
|
|
64
|
+
// Stripe-wide file-host/bucket policy is justified. Errors and diagnostic
|
|
65
|
+
// paths can have false positives for ordinary code/key links.
|
|
51
66
|
import { redact, refuse, safe, vendorErrors, } from "../_shared/rest/value-safety.js";
|
|
52
67
|
const EPHEMERAL_KEY = "It mints an ephemeral API key whose `secret` makes authorized Stripe API requests for a customer or Issuing card.";
|
|
53
68
|
const CONNECTION_TOKEN = "It mints a Terminal connection token whose `secret` connects a reader SDK to this account.";
|
|
@@ -698,6 +713,13 @@ const RESOURCES = {
|
|
|
698
713
|
},
|
|
699
714
|
};
|
|
700
715
|
export const STRIPE_VALUE_SAFETY = {
|
|
716
|
+
embeddedUrls: {
|
|
717
|
+
resources: {
|
|
718
|
+
key: "object",
|
|
719
|
+
partials: ["previous_attributes"],
|
|
720
|
+
paths: { charge: ["failure_message", "outcome.seller_message"] },
|
|
721
|
+
},
|
|
722
|
+
},
|
|
701
723
|
title: "Stripe",
|
|
702
724
|
operations: OPERATIONS,
|
|
703
725
|
fields: FIELDS,
|
|
@@ -3,7 +3,7 @@ import { ConnectorCallError } from "../../errors.js";
|
|
|
3
3
|
import { OperationIndex } from "../_shared/rest/operation-index.js";
|
|
4
4
|
import { callRest, restCall, unknownOutcome, restTransport, } from "../_shared/rest/tools.js";
|
|
5
5
|
import { openapi } from "./openapi.generated.js";
|
|
6
|
-
import { valueSafety } from "../_shared/rest/value-safety.js";
|
|
6
|
+
import { valueSafety, withholdEmbeddedUrlsIn, refuseWithheldLinks, refuseWithheldUpload, refuseWithheldBase64, } from "../_shared/rest/value-safety.js";
|
|
7
7
|
import { VERCEL_VALUE_SAFETY } from "./value-safety.js";
|
|
8
8
|
/** Vercel's public REST origin. Override only for a proxy or test double. */
|
|
9
9
|
export const VERCEL_API_BASE_URL = "https://api.vercel.com";
|
|
@@ -89,7 +89,7 @@ function reviewedCode(value) {
|
|
|
89
89
|
* value: it says the status, a reviewed code, and what to do.
|
|
90
90
|
*/
|
|
91
91
|
function vercelFailure(status, headers, payload, family = false) {
|
|
92
|
-
const root = record(payload);
|
|
92
|
+
const root = record(withholdEmbeddedUrlsIn(payload));
|
|
93
93
|
const error = record(root["error"]);
|
|
94
94
|
const code = text(error["code"]);
|
|
95
95
|
const message = text(error["message"])?.trim() ?? text(root["message"])?.trim() ?? `Vercel returned HTTP ${status}.`;
|
|
@@ -456,6 +456,22 @@ export function vercelRest(options) {
|
|
|
456
456
|
failure: (status, headers, body, call) => vercelFailure(status, headers, body, call ? SAFETY.withholdsErrors(call.op) : false),
|
|
457
457
|
readPosts: READ_POSTS,
|
|
458
458
|
valueSafety: SAFETY,
|
|
459
|
+
async admit(call) {
|
|
460
|
+
if (call.method === "GET" ||
|
|
461
|
+
call.method === "HEAD" ||
|
|
462
|
+
READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
|
|
463
|
+
return;
|
|
464
|
+
refuseWithheldLinks([call.body, call.query, call.path], "Vercel");
|
|
465
|
+
if (call.method === "POST" && call.op.path === "/v13/deployments") {
|
|
466
|
+
const files = record(call.body)["files"];
|
|
467
|
+
if (Array.isArray(files)) {
|
|
468
|
+
for (const file of files) {
|
|
469
|
+
if (record(file)["encoding"] === "base64")
|
|
470
|
+
await refuseWithheldBase64(record(file)["data"], "Vercel");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
459
475
|
refuse,
|
|
460
476
|
scope(call) {
|
|
461
477
|
const query = { ...call.query };
|
|
@@ -909,6 +925,8 @@ export function vercelRest(options) {
|
|
|
909
925
|
// Decode first, then validate what was supplied: a required binary body
|
|
910
926
|
// is present whenever bytes are (the index types it as a string).
|
|
911
927
|
const bytes = decodeUpload(args);
|
|
928
|
+
await vendor.admit?.(call, ctx);
|
|
929
|
+
await refuseWithheldUpload(bytes, "Vercel");
|
|
912
930
|
operations.check(call.op, call.query, String(args["base64Body"] ?? args["textBody"]));
|
|
913
931
|
const digest = call.op.path === "/v2/files" ? await sha1(bytes) : undefined;
|
|
914
932
|
const query = {};
|
|
@@ -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- `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",
|
|
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",
|
|
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
|
};
|
|
@@ -23,6 +23,19 @@
|
|
|
23
23
|
//
|
|
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
|
+
// Embedded URL verdict (#812): reviewed paths + global signing parameters.
|
|
27
|
+
// Evidence: pinned index/candidates sha256:32399b3ed273 identify deployment
|
|
28
|
+
// reads/creation/listing, build events and runtime logs; the operation/path
|
|
29
|
+
// list below scans errorMessage, deployments.errorMessage, payload.text/text
|
|
30
|
+
// and message respectively, both lists and individual named-stream rows.
|
|
31
|
+
// All vendor error text is scanned too. These diagnostics can have false
|
|
32
|
+
// positives for ordinary code/key links. Deployment meta, descriptions and
|
|
33
|
+
// other application content retain generic query parameters byte for byte.
|
|
34
|
+
// Literal X-Amz-/X-Goog- Signature/Credential/Security-Token values are
|
|
35
|
+
// withheld in every string. Existing structured-capability rules still apply.
|
|
36
|
+
// The original full response document is unavailable at its unversioned URL:
|
|
37
|
+
// current upstream differs and is supplementary evidence only. This is not a
|
|
38
|
+
// claim of complete pinned-response review or of a Vercel-wide host policy.
|
|
26
39
|
import { redact, refuse as refusal, safe, under, } from "../_shared/rest/value-safety.js";
|
|
27
40
|
/** A refusal's reason reads as a sentence about the operation: "It mints …". */
|
|
28
41
|
const refuse = (reason) => refusal(`It ${reason}`);
|
|
@@ -287,6 +300,15 @@ const VALUE_SAFETY = {
|
|
|
287
300
|
* names, which the heuristic leaves.
|
|
288
301
|
*/
|
|
289
302
|
export const VERCEL_VALUE_SAFETY = {
|
|
303
|
+
embeddedUrls: {
|
|
304
|
+
operations: {
|
|
305
|
+
"GET /v13/deployments/{idOrUrl}": ["errorMessage"],
|
|
306
|
+
"POST /v13/deployments": ["errorMessage"],
|
|
307
|
+
"GET /v7/deployments": ["deployments.errorMessage"],
|
|
308
|
+
"GET /v3/deployments/{idOrUrl}/events": ["payload.text", "text"],
|
|
309
|
+
"GET /v1/projects/{projectId}/deployments/{deploymentId}/runtime-logs": ["message"],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
290
312
|
title: "Vercel",
|
|
291
313
|
operations: VALUE_SAFETY,
|
|
292
314
|
envBodies: /^\/v\d+\/(?:env(?:\/|$)|projects\/\{[^}]+\}\/env(?:\/|$))/,
|
package/dist/registry.d.ts
CHANGED
|
@@ -130,14 +130,13 @@ export interface RegistryView {
|
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* Connector id → the only tool names this view may see on it. A connector
|
|
133
|
-
* absent from the map is visible whole. Derived from `
|
|
134
|
-
* addresses at
|
|
133
|
+
* absent from the map is visible whole. Derived from a pool's `tools`
|
|
134
|
+
* addresses at construction; never from caller input.
|
|
135
135
|
*/
|
|
136
136
|
export type ToolAccess = ReadonlyMap<string, ReadonlySet<string>>;
|
|
137
137
|
export interface RegistryScope {
|
|
138
138
|
connectorIds: "all" | readonly string[];
|
|
139
139
|
toolAccess?: ToolAccess;
|
|
140
|
-
guardedToolAccess?: ToolAccess;
|
|
141
140
|
subjectKey?: string;
|
|
142
141
|
principalKey?: string;
|
|
143
142
|
endpoint?: string;
|
package/dist/registry.js
CHANGED
|
@@ -277,7 +277,7 @@ export class Registry {
|
|
|
277
277
|
throw new Error(`Connector "${c.id}" declares both staticTools and a classification; ` + "annotate static tools directly.");
|
|
278
278
|
}
|
|
279
279
|
this.connectors.set(c.id, c);
|
|
280
|
-
if (c.callAdmission) {
|
|
280
|
+
if (c.callAdmission !== undefined) {
|
|
281
281
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
282
282
|
}
|
|
283
283
|
}
|
|
@@ -343,7 +343,7 @@ export class Registry {
|
|
|
343
343
|
const requested = scope.connectorIds === "all" ? new Set(this.connectors.keys()) : new Set(scope.connectorIds);
|
|
344
344
|
for (const id of requested) {
|
|
345
345
|
if (!this.connectors.has(id)) {
|
|
346
|
-
throw new Error(`
|
|
346
|
+
throw new Error(`Registry scope names unknown connector "${id}"`);
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
return new ScopedRegistryView(this, requested, scope);
|
|
@@ -368,13 +368,13 @@ export class Registry {
|
|
|
368
368
|
this.warnedAbsentGrants.delete(oldest);
|
|
369
369
|
}
|
|
370
370
|
if (hasControlCharacters(toolName)) {
|
|
371
|
-
logFailure(this.opts.logger, "
|
|
371
|
+
logFailure(this.opts.logger, "pool tool grant is unreachable", failureRecord({ connector: connectorId }));
|
|
372
372
|
return;
|
|
373
373
|
}
|
|
374
374
|
// Grant names are operator data but may carry any non-control character;
|
|
375
375
|
// quote them so a line terminator a log reader honours cannot forge a line.
|
|
376
376
|
const quoted = JSON.stringify(key).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
377
|
-
this.opts.logger.warn(`
|
|
377
|
+
this.opts.logger.warn(`a pool grants ${quoted} but connector "${connectorId}" lists no such tool; the grant is unreachable`);
|
|
378
378
|
}
|
|
379
379
|
async storeOAuthHandoff(connectorId, state, principalKey) {
|
|
380
380
|
const key = oauthHandoffKeys.handoff(connectorId, await sha256Hex(state));
|
|
@@ -1005,13 +1005,13 @@ class ScopedRegistryView {
|
|
|
1005
1005
|
return this.root.listConnectors().filter((connector) => this.registryFor(connector.id) !== undefined);
|
|
1006
1006
|
}
|
|
1007
1007
|
canReadConnectorSkills(id) {
|
|
1008
|
-
return
|
|
1008
|
+
return this.registryFor(id) !== undefined && !this.scope.toolAccess?.has(id);
|
|
1009
1009
|
}
|
|
1010
1010
|
getConnector(id) {
|
|
1011
1011
|
return this.registryFor(id)?.getConnector(id);
|
|
1012
1012
|
}
|
|
1013
1013
|
getResourceConnector(id) {
|
|
1014
|
-
if (this.scope.toolAccess?.has(id)
|
|
1014
|
+
if (this.scope.toolAccess?.has(id))
|
|
1015
1015
|
return undefined;
|
|
1016
1016
|
return this.getConnector(id);
|
|
1017
1017
|
}
|
|
@@ -1028,14 +1028,13 @@ class ScopedRegistryView {
|
|
|
1028
1028
|
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1029
1029
|
}
|
|
1030
1030
|
const tools = await registry.getTools(args[0], args[1], args[2], args[3], this.scope);
|
|
1031
|
-
const
|
|
1032
|
-
const granted = this.scope.toolAccess?.get(args[0]) ?? guarded;
|
|
1031
|
+
const granted = this.scope.toolAccess?.get(args[0]);
|
|
1033
1032
|
if (!granted)
|
|
1034
1033
|
return tools;
|
|
1035
1034
|
// Every consumer — search, describe, call_tool, and a program's
|
|
1036
1035
|
// connecta.call — resolves through this list, so an ungranted tool is
|
|
1037
1036
|
// indistinguishable from one the connector never had.
|
|
1038
|
-
const visible = tools.filter((tool) => granted.has(tool.name)
|
|
1037
|
+
const visible = tools.filter((tool) => granted.has(tool.name));
|
|
1039
1038
|
if (visible.length < granted.size) {
|
|
1040
1039
|
const present = new Set(tools.map((tool) => tool.name));
|
|
1041
1040
|
for (const name of granted) {
|
|
@@ -1,62 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { AccessTokenError, isSlotRevision } from "../access-tokens.js";
|
|
2
|
+
import { authorizeUiIdentity, boundedText, isSameOrigin, managesAgentTokens, privateJson, } from "./shared.js";
|
|
3
|
+
/** The longest body, in characters, a token mutation accepts: `{ "expect": "<revision>" }` with room to spare. */
|
|
4
|
+
const MAX_BODY_CHARS = 1_024;
|
|
5
|
+
/**
|
|
6
|
+
* The slot revision a mutation names. POST takes it only as a JSON body,
|
|
7
|
+
* `{ "expect": "<revision>" }`; DELETE takes that body or, for clients that
|
|
8
|
+
* cannot send a body with DELETE, `?expect=<revision>`, but not both. Read
|
|
9
|
+
* only after the caller is authorized.
|
|
10
|
+
*/
|
|
11
|
+
async function expectedRevision(request) {
|
|
12
|
+
const query = new URL(request.url).searchParams.getAll("expect");
|
|
13
|
+
if (query.length > 0 && request.method !== "DELETE") {
|
|
14
|
+
return { status: 400, error: "expect belongs in the JSON body" };
|
|
8
15
|
}
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const raw = await boundedText(request, MAX_BODY_CHARS);
|
|
17
|
+
if (raw === undefined)
|
|
18
|
+
return { status: 413, error: "request body is too large" };
|
|
19
|
+
let candidate;
|
|
20
|
+
if (raw !== "") {
|
|
21
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
22
|
+
return { status: 415, error: "Content-Type must be application/json" };
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
13
25
|
try {
|
|
14
|
-
|
|
15
|
-
const { done, value } = await reader.read();
|
|
16
|
-
if (done)
|
|
17
|
-
break;
|
|
18
|
-
size += value.byteLength;
|
|
19
|
-
if (size > 1_000) {
|
|
20
|
-
reader.cancel().catch(() => { });
|
|
21
|
-
return { ok: false, response: privateJson({ error: "request body is too large" }, { status: 413 }) };
|
|
22
|
-
}
|
|
23
|
-
chunks.push(value);
|
|
24
|
-
}
|
|
26
|
+
body = JSON.parse(raw);
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
catch {
|
|
29
|
+
return { status: 400, error: "invalid JSON body" };
|
|
28
30
|
}
|
|
31
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
32
|
+
return { status: 400, error: "invalid JSON body" };
|
|
33
|
+
}
|
|
34
|
+
if (query.length > 0)
|
|
35
|
+
return { status: 400, error: "give expect once, in the body or the query" };
|
|
36
|
+
candidate = body.expect;
|
|
29
37
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for (const chunk of chunks) {
|
|
33
|
-
bytes.set(chunk, offset);
|
|
34
|
-
offset += chunk.byteLength;
|
|
35
|
-
}
|
|
36
|
-
const raw = new TextDecoder().decode(bytes);
|
|
37
|
-
if (raw.length > 1_000) {
|
|
38
|
-
return {
|
|
39
|
-
ok: false,
|
|
40
|
-
response: privateJson({ error: "request body is too large" }, { status: 413 }),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const body = JSON.parse(raw);
|
|
45
|
-
return { ok: true, name: body.name };
|
|
38
|
+
else if (query.length === 1) {
|
|
39
|
+
candidate = query[0];
|
|
46
40
|
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
ok: false,
|
|
50
|
-
response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
|
|
51
|
-
};
|
|
41
|
+
if (candidate === undefined) {
|
|
42
|
+
return { status: 400, error: "expect is required: the slot revision from GET /ui/access-tokens" };
|
|
52
43
|
}
|
|
44
|
+
return isSlotRevision(candidate) ? { expect: candidate } : { status: 400, error: "expect is not a slot revision" };
|
|
53
45
|
}
|
|
54
46
|
/**
|
|
55
|
-
* Interactive-operator lifecycle for
|
|
47
|
+
* Interactive-operator lifecycle for agent tokens: list one slot per declared
|
|
48
|
+
* agent, issue or rotate its token, and revoke it. Each mutation names the
|
|
49
|
+
* slot revision it decided from, so a stale view refuses with 409
|
|
50
|
+
* `slot_changed` instead of issuing over a token or rotating over someone
|
|
51
|
+
* else's fresh rotation. Agents themselves come only
|
|
52
|
+
* from code, so nothing here creates, renames, or rebinds one. The token itself
|
|
56
53
|
* is deliberately never an administrator credential and cannot reach here.
|
|
57
54
|
*/
|
|
58
55
|
export async function routeAccessTokens(context, manager) {
|
|
59
|
-
const match = /^\/ui\/access-tokens(?:\/([
|
|
56
|
+
const match = /^\/ui\/access-tokens(?:\/([a-z0-9_-]{1,64}))?$/.exec(context.path);
|
|
60
57
|
if (!match)
|
|
61
58
|
return null;
|
|
62
59
|
const { request, baseUrl, opts } = context;
|
|
@@ -67,41 +64,51 @@ export async function routeAccessTokens(context, manager) {
|
|
|
67
64
|
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
68
65
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
69
66
|
}
|
|
70
|
-
const admin = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "access token management", context.runtimeContext
|
|
67
|
+
const admin = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "access token management", context.runtimeContext);
|
|
71
68
|
if (!admin.ok)
|
|
72
69
|
return admin.response;
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
// Interactive maintainers alone manage agent tokens (#837); anyone else
|
|
71
|
+
// gets the same private 404 every operator route gives them.
|
|
72
|
+
if (!managesAgentTokens(opts, admin) || !admin.identity.principal) {
|
|
73
|
+
return privateJson({ error: "not found" }, { status: 404 });
|
|
75
74
|
}
|
|
76
|
-
const
|
|
75
|
+
const operator = `${admin.identity.principal.namespace}:${admin.identity.principal.id}`;
|
|
76
|
+
const name = match[1];
|
|
77
77
|
try {
|
|
78
|
-
if (!
|
|
79
|
-
return privateJson({
|
|
78
|
+
if (!name && request.method === "GET") {
|
|
79
|
+
return privateJson({ agents: await manager.list() });
|
|
80
80
|
}
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
if (!input.ok)
|
|
84
|
-
return input.response;
|
|
85
|
-
return privateJson(await manager.create(input.name, admin.identity.principal), { status: 201 });
|
|
81
|
+
if (!name || (request.method !== "POST" && request.method !== "DELETE")) {
|
|
82
|
+
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
const expected = await expectedRevision(request);
|
|
85
|
+
if (!("expect" in expected))
|
|
86
|
+
return privateJson({ error: expected.error }, { status: expected.status });
|
|
87
|
+
// The request's signal bounds each mutation: a caller that leaves before
|
|
88
|
+
// the slot commit publishes nothing, and one that leaves after a rotation
|
|
89
|
+
// commits has the slot revoked, since its secret cannot be delivered.
|
|
90
|
+
const options = { signal: request.signal, expect: expected.expect };
|
|
91
|
+
if (request.method === "POST")
|
|
92
|
+
return privateJson(await manager.rotate(name, operator, options));
|
|
93
|
+
return privateJson({ agent: await manager.revoke(name, options) });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof AccessTokenError && error.code === "unknown_agent") {
|
|
97
|
+
return privateJson({ error: "unknown agent" }, { status: 404 });
|
|
95
98
|
}
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
return accessToken
|
|
99
|
-
? privateJson({ accessToken })
|
|
100
|
-
: privateJson({ error: "unknown access token" }, { status: 404 });
|
|
99
|
+
if (error instanceof AccessTokenError && error.code === "aborted") {
|
|
100
|
+
return privateJson({ error: "request ended" }, { status: 499 });
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (error instanceof AccessTokenError && error.code === "outcome_unknown") {
|
|
103
|
+
// A fixed, operation-specific sentence; it never carries storage text.
|
|
104
|
+
return privateJson({ error: error.message }, { status: 503 });
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof AccessTokenError && error.code === "slot_changed") {
|
|
107
|
+
return privateJson({ error: "The agent's token slot changed; reload and retry", code: "slot_changed" }, { status: 409 });
|
|
108
|
+
}
|
|
109
|
+
if (error instanceof AccessTokenError && error.code === "conflict") {
|
|
110
|
+
return privateJson({ error: "The agent's token changed meanwhile; reload and retry" }, { status: 409 });
|
|
111
|
+
}
|
|
112
|
+
return privateJson({ error: "Access token operation failed; check storage" }, { status: 400 });
|
|
106
113
|
}
|
|
107
114
|
}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import { intersectAccess } from "../connector-access.js";
|
|
2
1
|
import { recordedToolName } from "../operator-record.js";
|
|
3
|
-
/**
|
|
4
|
-
function activityToolVisible(
|
|
5
|
-
const allowed =
|
|
6
|
-
return
|
|
7
|
-
(!allowed || allowed.has(event.toolName)) &&
|
|
8
|
-
(!authz.guardedToolAccess?.get(event.connectorId)?.has(event.toolName) || event.classification === "read"));
|
|
2
|
+
/** A recorded tool is shown only when its name passes the record grammar and its endpoint granted it. */
|
|
3
|
+
function activityToolVisible(access, event) {
|
|
4
|
+
const allowed = access.toolAccess?.get(event.connectorId);
|
|
5
|
+
return recordedToolName({ name: event.toolName }) === event.toolName && (!allowed || allowed.has(event.toolName));
|
|
9
6
|
}
|
|
10
7
|
/**
|
|
11
|
-
* One disclosure rule for the timeline and the UI's last-call overlay.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* and
|
|
16
|
-
*
|
|
8
|
+
* One disclosure rule for the timeline and the UI's last-call overlay. Only
|
|
9
|
+
* maintainers read activity (#837), and only after the optional
|
|
10
|
+
* read gate. They may see other principals' shared-connector history,
|
|
11
|
+
* including request IDs, client name/version, and actor kind/id/namespace, on
|
|
12
|
+
* plain /mcp and in every declared pool, within the tools that pool declares.
|
|
13
|
+
* Interactive reads may add directory actor labels; machine reads do not.
|
|
14
|
+
* Personal history requires a matching principal owner. A row recorded in a
|
|
15
|
+
* pool that is no longer declared is withheld.
|
|
17
16
|
*/
|
|
18
|
-
export function activityEventVisible(context, authz, registry,
|
|
17
|
+
export function activityEventVisible(context, authz, registry, event) {
|
|
19
18
|
const connector = registry.getConnector(event.connectorId);
|
|
20
|
-
if (!connector
|
|
19
|
+
if (!connector)
|
|
21
20
|
return false;
|
|
22
21
|
if (connector.authScope === "personal" || (event.kind === "catalog_drift" && event.actorBasis === "principal")) {
|
|
23
22
|
const owner = authz.identity.principal;
|
|
@@ -27,10 +26,10 @@ export function activityEventVisible(context, authz, registry, admittedPools, ev
|
|
|
27
26
|
event.actor.namespace !== owner.namespace)
|
|
28
27
|
return false;
|
|
29
28
|
}
|
|
30
|
-
const pool = event.pool ? context.opts.pools.get(event.pool) : undefined;
|
|
31
|
-
if (event.pool && !pool)
|
|
29
|
+
const pool = event.pool !== undefined ? context.opts.pools.get(event.pool) : undefined;
|
|
30
|
+
if (event.pool !== undefined && !pool)
|
|
32
31
|
return false;
|
|
33
|
-
const access = pool ?
|
|
32
|
+
const access = pool ? pool.access : { connectorIds: "all" };
|
|
34
33
|
if (access.connectorIds !== "all" && !access.connectorIds.includes(event.connectorId))
|
|
35
34
|
return false;
|
|
36
35
|
// Connector-wide counts reveal tools outside an address-only grant.
|