@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { Routes, routePhrases } from "../../agent-routes.js";
|
|
2
|
+
import { asRecord, asArray } from "../_shared/json.js";
|
|
3
|
+
// Tithe.ly's giving API (v1) as a kit vendor: the operations,
|
|
2
4
|
// what its statuses mean, its credential, its in-band refusal, the probe, and
|
|
3
5
|
// the value-safety review. Facts from the per-operation reference at
|
|
4
6
|
// https://docs.tithe.ly/reference/introduction, each marked where it is
|
|
@@ -388,9 +390,10 @@ function keyPairProblem(values) {
|
|
|
388
390
|
/** Operator-held Basic keys, resolved per request; a mismatched pair is refused before anything leaves. */
|
|
389
391
|
export const authenticate = Effect.gen(function* () {
|
|
390
392
|
const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
|
|
393
|
+
const routes = yield* Routes;
|
|
391
394
|
const pair = keyPairProblem(values);
|
|
392
395
|
if (typeof pair === "string") {
|
|
393
|
-
return yield* failed(new ConnectorCallError("auth_required", `${pair}
|
|
396
|
+
return yield* failed(new ConnectorCallError("auth_required", `${pair} ${routePhrases(routes).authorizeFor("recovery options")}; an operator adds the keys on this connection in the operator UI.`));
|
|
394
397
|
}
|
|
395
398
|
return { Authorization: `Basic ${btoa(`${pair.publicKey}:${pair.privateKey}`)}` };
|
|
396
399
|
});
|
|
@@ -509,12 +512,6 @@ export function tithelySafety(sensitiveFields) {
|
|
|
509
512
|
}
|
|
510
513
|
// --- Shared readers ------------------------------------------------------------------
|
|
511
514
|
const failed = (error) => Effect.fail(new Refusal({ error }));
|
|
512
|
-
export function asRecord(value) {
|
|
513
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
514
|
-
}
|
|
515
|
-
export function asArray(value) {
|
|
516
|
-
return Array.isArray(value) ? value : [];
|
|
517
|
-
}
|
|
518
515
|
export function text(value) {
|
|
519
516
|
return typeof value === "string" && value !== "" ? value : undefined;
|
|
520
517
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { readOnlyNotice } from "../_shared/guide.js";
|
|
1
2
|
import { skill } from "./skill.generated.js";
|
|
2
3
|
/**
|
|
3
|
-
* Tithe.ly's giving API (v1) on the provider kit
|
|
4
|
+
* Tithe.ly's giving API (v1) on the provider kit. No SDK
|
|
4
5
|
* exists to depend on, and none would be welcome: this is a payments API, and
|
|
5
6
|
* every tool was chosen and written, not generated (decision record 0001:
|
|
6
7
|
* hand-authored tools). The vendor lives in `api.ts` and the tools in
|
|
@@ -67,7 +68,7 @@ function usageGuide(options) {
|
|
|
67
68
|
sensitiveFields(options) === "return"
|
|
68
69
|
? "This connection returns organizations' payout bank and legal-contact details through tithely_api_read; the named reads still omit them."
|
|
69
70
|
: "This connection withholds organizations' payout bank and legal-contact details from every result.",
|
|
70
|
-
...(options.readOnly === true
|
|
71
|
+
...readOnlyNotice(options.readOnly === true),
|
|
71
72
|
...(options.defaultPageSize !== undefined
|
|
72
73
|
? [`List tools return ${options.defaultPageSize} rows by default on this connection.`]
|
|
73
74
|
: []),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Giving data is pastoral\n\nDonor names, emails, addresses, and gift amounts are pastoral data. Reduce\ninside `execute_code`: page, filter, and total there, and return the\naggregate (sums by fund, month, or donor count), not donor rows, unless the\ntask needs named people.\n\n## Ids and joins\n\n- Ids carry their kind: `org_` organization, `user_` donor account, `ch_`\n charge, `rc_` recurring gift, `pm_` payment method. Read them from the list\n tools; never guess one.\n- Tithe.ly lists charges and recurring gifts only for one organization or one\n donor, so pass `organizationId` or `accountId`.\n- A charge carries its donor's name and email, its organization's name, and\n its payment method, so most joins need no second read.\n- `list_accounts` covers every account these keys can see; `scope: \"api\"`\n narrows it to accounts created through this API.\n- The named reads project Tithe.ly's records. A field a projection omits, such\n as an account's phone and postal address, comes back from `tithely_api_read`\n on that record's path.\n\n## Money, dates and funds\n\n- A fund is a Tithe.ly giving type: `giving_type` in Tithe.ly's records,\n `fund` in projections.\n- Projections give amounts as integer `…Cents` and dates as ISO 8601 UTC\n `…At`. Tithe.ly's own records, from `tithely_api_read`, keep string cents\n and Unix seconds.\n- `depositedAt` may be `\"pending\"`. There is no deposits resource: net amount\n and fees live on each charge.\n- A charge a recurring gift produced has `recurring: true`; a one-time gift\n has `recurring: false`.\n\n## Writes\n\n- Every write moves money or changes a donor's records. Bodies are form\n fields in integer cents and Unix seconds.\n- `POST /refunds/{charge_id}` refunds a whole charge.\n `DELETE /recurring/{recurring_id}` stops future charges and refunds nothing.\n- Card and bank tokens, which `POST /charge-once` and `POST /payment-methods`\n need, come only from Tithely.js in a donor's browser. An agent cannot\n create one.\n- Tithe.ly reports most refusals as HTTP 200 with `status: \"fail\"` and a\n reason but no code. They arrive as `connector_call_failed`; read the reason\n before deciding what to do next.\n\n## Not available\n\nOnly Tithe.ly's API v1 is reachable; its undocumented v2 is not.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_accounts`, `list_charges`, `list_organizations`, `list_payment_methods`, `list_recurring_charges`, `tithely_api_details`, `tithely_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 99). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `tithely_api_search` finds this API's operations and reads no account data, `tithely_api_details` shows one operation's contract, and `tithely_api_read` calls a reviewed read. `tithely_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
3
|
+
"content": "## Giving data is pastoral\n\nDonor names, emails, addresses, and gift amounts are pastoral data. Reduce\ninside `execute_code`: page, filter, and total there, and return the\naggregate (sums by fund, month, or donor count), not donor rows, unless the\ntask needs named people.\n\n## Ids and joins\n\n- Ids carry their kind: `org_` organization, `user_` donor account, `ch_`\n charge, `rc_` recurring gift, `pm_` payment method. Read them from the list\n tools; never guess one.\n- Tithe.ly lists charges and recurring gifts only for one organization or one\n donor, so pass `organizationId` or `accountId`.\n- A charge carries its donor's name and email, its organization's name, and\n its payment method, so most joins need no second read.\n- `list_accounts` covers every account these keys can see; `scope: \"api\"`\n narrows it to accounts created through this API.\n- The named reads project Tithe.ly's records. A field a projection omits, such\n as an account's phone and postal address, comes back from `tithely_api_read`\n on that record's path.\n\n## Money, dates and funds\n\n- A fund is a Tithe.ly giving type: `giving_type` in Tithe.ly's records,\n `fund` in projections.\n- Projections give amounts as integer `…Cents` and dates as ISO 8601 UTC\n `…At`. Tithe.ly's own records, from `tithely_api_read`, keep string cents\n and Unix seconds.\n- `depositedAt` may be `\"pending\"`. There is no deposits resource: net amount\n and fees live on each charge.\n- A charge a recurring gift produced has `recurring: true`; a one-time gift\n has `recurring: false`.\n\n## Writes\n\n- Every write moves money or changes a donor's records. Bodies are form\n fields in integer cents and Unix seconds.\n- `POST /refunds/{charge_id}` refunds a whole charge.\n `DELETE /recurring/{recurring_id}` stops future charges and refunds nothing.\n- Card and bank tokens, which `POST /charge-once` and `POST /payment-methods`\n need, come only from Tithely.js in a donor's browser. An agent cannot\n create one.\n- Tithe.ly reports most refusals as HTTP 200 with `status: \"fail\"` and a\n reason but no code. They arrive as `connector_call_failed`; read the reason\n before deciding what to do next.\n\n## Not available\n\nOnly Tithe.ly's API v1 is reachable; its undocumented v2 is not.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_accounts`, `list_charges`, `list_organizations`, `list_payment_methods`, `list_recurring_charges`, `tithely_api_details`, `tithely_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 99). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `tithely_api_search` finds this API's operations and reads no account data, `tithely_api_details` shows one operation's contract, and `tithely_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `tithely_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. An unknown id, or one these credentials cannot reach, fails as `connector_call_failed`; this API does not tell the two apart. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Account instructions"
|
|
5
5
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { compact as compactDefined } from "../_shared/json.js";
|
|
2
|
+
import { asArray, asRecord } from "../_shared/json.js";
|
|
3
|
+
// Tithe.ly's curated tools on the provider kit and their
|
|
2
4
|
// projections. Decision 0011's step 2: standard lists ({ items, page } with
|
|
3
5
|
// limit, cursor and fields), fields on every named read, and no `raw`: what a
|
|
4
6
|
// projection drops stays reachable through the standard hatch, which the kit
|
|
5
|
-
// builds from `api.ts`'s index and verdicts
|
|
7
|
+
// builds from `api.ts`'s index and verdicts. Each named read
|
|
6
8
|
// earns its place against the hatch with paging Tithe.ly cannot report
|
|
7
9
|
// (lookahead), units (cents, ISO dates), joins (donor, organization and
|
|
8
10
|
// payment method inline) or reconciliation of a known id after a write.
|
|
@@ -13,7 +15,7 @@ import { Refusal } from "../../http/errors.js";
|
|
|
13
15
|
import { Integer } from "../../http/json-schema.js";
|
|
14
16
|
import { lookahead } from "../../http/paging.js";
|
|
15
17
|
import { get as getTool, list } from "../../http/tools.js";
|
|
16
|
-
import {
|
|
18
|
+
import { entity, getCharge, getRecurring, listAccounts, listAllAccounts, listCharges, listOrganizations, listPaymentMethods, listRecurring, MAX_PAGE_SIZE, PAYOUT_CURRENCY, text, } from "./api.js";
|
|
17
19
|
// --- Schemas ---------------------------------------------------------------------
|
|
18
20
|
const Id = Schema.String.check(Schema.isMinLength(1));
|
|
19
21
|
const Text = Schema.optionalKey(Schema.String);
|
|
@@ -267,7 +269,7 @@ function seconds(value, name) {
|
|
|
267
269
|
return Effect.succeed(Math.floor(parsed / 1000));
|
|
268
270
|
}
|
|
269
271
|
function compact(value) {
|
|
270
|
-
return
|
|
272
|
+
return compactDefined(value);
|
|
271
273
|
}
|
|
272
274
|
/** Integer cents from Tithe.ly's string-or-number amounts. */
|
|
273
275
|
function cents(value) {
|
|
@@ -325,7 +327,7 @@ function projectOrganization(value) {
|
|
|
325
327
|
return compact({
|
|
326
328
|
organizationId: text(organization["organization_id"]),
|
|
327
329
|
name: text(organization["name"]),
|
|
328
|
-
// The giving widget id embeds Tithe.ly's giving form; the step-2 eval showed agents need it
|
|
330
|
+
// The giving widget id embeds Tithe.ly's giving form; the step-2 eval showed agents need it.
|
|
329
331
|
widgetId: text(organization["widget_id"]),
|
|
330
332
|
ownerAccountId: text(organization["account_id"]),
|
|
331
333
|
website: text(organization["website"]),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "../_shared/json.js";
|
|
1
2
|
// Vercel's pinned contract, authentication, failures and value-safety boundary.
|
|
2
3
|
import { Effect, Schema } from "effect";
|
|
3
4
|
import { ConnectorCallError } from "../../errors.js";
|
|
@@ -24,9 +25,6 @@ const READ_POSTS = [
|
|
|
24
25
|
["POST", "/v1/registrar/domains/search", "Searches domain availability and pricing; buys nothing."],
|
|
25
26
|
["POST", "/v8/artifacts", "Reads Remote Cache metadata for a list of artifact hashes; uploads nothing."],
|
|
26
27
|
];
|
|
27
|
-
export function isRecord(value) {
|
|
28
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
-
}
|
|
30
28
|
export function record(value) {
|
|
31
29
|
return isRecord(value) ? value : {};
|
|
32
30
|
}
|
|
@@ -36,9 +34,6 @@ export function text(value) {
|
|
|
36
34
|
export function invalid(message) {
|
|
37
35
|
throw new ConnectorCallError("invalid_args", message);
|
|
38
36
|
}
|
|
39
|
-
export function compact(value) {
|
|
40
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
41
|
-
}
|
|
42
37
|
function truthy(value) {
|
|
43
38
|
return value === true || value === 1 || (typeof value === "string" && /^(?:1|true)$/i.test(value.trim()));
|
|
44
39
|
}
|
|
@@ -461,7 +456,7 @@ export function transformGuide(guide, options) {
|
|
|
461
456
|
const base = Object.values(VERCEL_VERDICTS).filter((verdict) => verdict === "refuse").length;
|
|
462
457
|
const total = vercelIndex.ops.filter((op) => VERCEL_VERDICTS[`${op.method} ${op.path}`] === "refuse" ||
|
|
463
458
|
PIN_DISCOVERY_REFUSALS[`${op.method} ${op.path}`] !== undefined).length;
|
|
464
|
-
const sentence = `${base} operations are refused by design; use \`vercel_api_details\` with the method and path to
|
|
459
|
+
const sentence = `${base} operations are refused by design; use \`vercel_api_details\` with the method and path to see each summary and refusal,`;
|
|
465
460
|
const lines = guide.content.split("\n");
|
|
466
461
|
const targets = lines.flatMap((line, position) => line.startsWith("- **Hatch:** `vercel_api_search` ") && line.includes(sentence) ? [position] : []);
|
|
467
462
|
const position = targets[0];
|
|
@@ -4,7 +4,7 @@ import { ConnectorCallError } from "../../../errors.js";
|
|
|
4
4
|
import { Refusal } from "../../../http/errors.js";
|
|
5
5
|
import { Budget } from "../../../http/services.js";
|
|
6
6
|
import { checked } from "../../_shared/rest/kit.js";
|
|
7
|
-
|
|
7
|
+
const MAX_PIN_LOOKUPS = 8;
|
|
8
8
|
export const PIN_WRITE_STEPS = Object.freeze({
|
|
9
9
|
write: "once",
|
|
10
10
|
lookup1: "once",
|
|
@@ -214,7 +214,7 @@ const absent = {
|
|
|
214
214
|
//
|
|
215
215
|
// Residual risk: a secret a person typed into a free-text
|
|
216
216
|
// name, description, log line, or identifier is out of scope.
|
|
217
|
-
// Embedded URL verdict
|
|
217
|
+
// Embedded URL verdict: reviewed paths + global signing parameters.
|
|
218
218
|
// Evidence: pinned index/candidates sha256:5a702fd2dc76 identify deployment
|
|
219
219
|
// reads/creation/listing, build events and runtime logs; the operation/path
|
|
220
220
|
// list below scans errorMessage, deployments.errorMessage, payload.text/text
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_teams`, `vercel_api_details`, `vercel_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `vercel_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `vercel_api_search` finds this API's operations and reads no account data, `vercel_api_details` shows one operation's contract, and `vercel_api_read` calls a reviewed read. `vercel_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 29 operations are refused by design; use `vercel_api_details` with the method and path to
|
|
3
|
+
"content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `list_teams`, `vercel_api_details`, `vercel_api_search`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `vercel_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 20, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `vercel_api_search` finds this API's operations and reads no account data, `vercel_api_details` shows one operation's contract, and `vercel_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `vercel_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 29 operations are refused by design; use `vercel_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
|
|
4
4
|
"instructionsHeading": "Account instructions"
|
|
5
5
|
};
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { compact, isRecord } from "../_shared/json.js";
|
|
1
2
|
// Curated Vercel metadata and bounded logs through kit-owned call services.
|
|
2
3
|
import { Effect, Schema } from "effect";
|
|
3
4
|
import { ConnectorCallError } from "../../errors.js";
|
|
4
5
|
import { Refusal } from "../../http/errors.js";
|
|
5
6
|
import { read, write, list } from "../../http/tools.js";
|
|
6
|
-
import {
|
|
7
|
+
import { fieldsContract, selectFields } from "../../http/fields.js";
|
|
7
8
|
import { vercelPage } from "../../http/hatch-paging.js";
|
|
8
9
|
import { schema, typedParams } from "../_shared/rest/kit.js";
|
|
9
10
|
import { refuseWithheldUpload } from "../_shared/rest/value-safety.js";
|
|
10
11
|
import { rowStop } from "./lib/logs.js";
|
|
11
12
|
import { checked } from "../_shared/rest/kit.js";
|
|
12
|
-
import { admit,
|
|
13
|
+
import { admit, defaultTeam, invalid, named, queryOf, queryPairs, record, reviewedCode, restCall, text, vercelIndex, refuse, checkServer, } from "./api.js";
|
|
13
14
|
import { pinOptions, readPinnedTeam, PIN_WRITE_STEPS } from "./lib/pin.js";
|
|
14
15
|
const MAX_LOG_BYTES = 1024 * 1024;
|
|
15
16
|
const MAX_LOG_TEXT = 4_000;
|
|
@@ -65,6 +66,7 @@ const ENV_SCHEMA = {
|
|
|
65
66
|
},
|
|
66
67
|
required: ["id", "key", "type"],
|
|
67
68
|
};
|
|
69
|
+
const ENV_FIELDS = fieldsContract("item", ENV_SCHEMA, "Values cannot be restored.");
|
|
68
70
|
/** Deliberately omits `value`, even though redaction already removed it. */
|
|
69
71
|
function envMetadata(value) {
|
|
70
72
|
const variable = record(value);
|
|
@@ -157,7 +159,7 @@ export function vercelTools({ connection, options, }) {
|
|
|
157
159
|
type: "array",
|
|
158
160
|
maxItems: 50,
|
|
159
161
|
items: { type: "string", minLength: 1, maxLength: 200 },
|
|
160
|
-
description:
|
|
162
|
+
description: ENV_FIELDS.description,
|
|
161
163
|
},
|
|
162
164
|
}, ["projectId"]),
|
|
163
165
|
outputSchema: {
|
|
@@ -170,7 +172,7 @@ export function vercelTools({ connection, options, }) {
|
|
|
170
172
|
},
|
|
171
173
|
run: (args) => Effect.gen(function* () {
|
|
172
174
|
const fields = args["fields"];
|
|
173
|
-
const problem =
|
|
175
|
+
const problem = ENV_FIELDS.problem(fields);
|
|
174
176
|
if (problem)
|
|
175
177
|
throw problem;
|
|
176
178
|
const { data } = yield* send("GET", `/v10/projects/${segment(args["projectId"])}/env`, {
|
package/dist/redaction-marker.js
CHANGED
|
@@ -7,7 +7,7 @@ export const REDACTED = "[redacted]";
|
|
|
7
7
|
* secrets from every value of a vendor body before result safety reads it, so a rule that decides
|
|
8
8
|
* on a value (a type discriminator, a record's label or type, a URL's scheme or parameter names,
|
|
9
9
|
* a Content-Type) can find the placeholder where the vendor wrote what the rule matches.
|
|
10
|
-
* Such a decision takes its most protective outcome
|
|
10
|
+
* Such a decision takes its most protective outcome.
|
|
11
11
|
*/
|
|
12
12
|
export function redactionAltered(value) {
|
|
13
13
|
return typeof value === "string" && value.includes(REDACTED);
|
|
@@ -92,7 +92,7 @@ function credentialUnit(code) {
|
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* The text right after the placeholder at `at` that may be the rest of a credential whose public
|
|
95
|
-
* prefix redaction took
|
|
95
|
+
* prefix redaction took: up to the first unit `ends` accepts, reading later placeholders
|
|
96
96
|
* whole so none is split. Redaction can take `sk_live_`, `whsec_`, `Bearer `, or a signing
|
|
97
97
|
* parameter's name and `=`, leaving what follows; with that prefix known, any remainder rebuilds
|
|
98
98
|
* the credential, however short or encoded, so `readable` holds whenever it carries any base64 or
|
package/dist/registry.d.ts
CHANGED
|
@@ -71,8 +71,10 @@ export interface RegistryOptions {
|
|
|
71
71
|
maxStashBytes?: number;
|
|
72
72
|
maxStashEntries?: number;
|
|
73
73
|
} | undefined;
|
|
74
|
+
/** Route names every context carries unless a call names its own; meta by default. */
|
|
75
|
+
routes?: import("./types.js").AgentRouteNames | undefined;
|
|
74
76
|
}
|
|
75
|
-
export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeoutMs" | "defer">;
|
|
77
|
+
export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeoutMs" | "defer" | "routes">;
|
|
76
78
|
/**
|
|
77
79
|
* The registry surface a per-connection MCP server consumes: every meta-tool
|
|
78
80
|
* (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
|
|
@@ -84,6 +86,7 @@ export type ConnectorOperationOptions = Pick<ConnectorContext, "signal" | "timeo
|
|
|
84
86
|
*/
|
|
85
87
|
/** Host-admitted identity and endpoint bindings for result paging. */
|
|
86
88
|
export interface ResultIdentity {
|
|
89
|
+
pool: string | null;
|
|
87
90
|
subject: string | null;
|
|
88
91
|
principal: string | null;
|
|
89
92
|
endpoint: string;
|
|
@@ -124,8 +127,11 @@ export interface RegistryView {
|
|
|
124
127
|
recheckResultAccess(dependencies: readonly ProgramResultDependency[], classification: "read" | "write", signal?: AbortSignal): Promise<boolean>;
|
|
125
128
|
/** Reserve deployment-wide capacity before writing a paging envelope's chunks. */
|
|
126
129
|
stashResult(id: string, chunks: readonly string[], ttlSeconds: number): Promise<boolean>;
|
|
127
|
-
/**
|
|
128
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Local declared-vs-stored credential mismatch, with no downstream I/O. With
|
|
132
|
+
* `requestScope`, the read joins that request's read memo.
|
|
133
|
+
*/
|
|
134
|
+
credentialDriftFor(id: string, requestScope?: object): Promise<string | undefined>;
|
|
129
135
|
/** Value-free shape learned from successful calls, never a provider declaration. */
|
|
130
136
|
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
131
137
|
/** Passively learn one successful unwrapped result; failures stay isolated. */
|
|
@@ -240,7 +246,7 @@ export declare class Registry implements RegistryView {
|
|
|
240
246
|
*
|
|
241
247
|
* `chunks[n]` lands on `resultKeys.chunk(id, n)` inside `partition` — the
|
|
242
248
|
* layout connecta.result reads back, so a page fetches only the chunks it covers
|
|
243
|
-
* instead of the whole stored result
|
|
249
|
+
* instead of the whole stored result. Chunking is a read-cost
|
|
244
250
|
* decision, not a capacity one: however many keys an envelope occupies, it
|
|
245
251
|
* is one stash entry charged its total ASCII length.
|
|
246
252
|
*
|
|
@@ -287,7 +293,7 @@ export declare class Registry implements RegistryView {
|
|
|
287
293
|
describeStaticTools(id: string): ToolDef[] | undefined;
|
|
288
294
|
private validateClassification;
|
|
289
295
|
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, scope?: RegistryScope): Promise<ToolDef[]>;
|
|
290
|
-
credentialDriftFor(id: string): Promise<string | undefined>;
|
|
296
|
+
credentialDriftFor(id: string, requestScope?: object): Promise<string | undefined>;
|
|
291
297
|
/** Age of a complete cache entry; reads never refresh it. */
|
|
292
298
|
catalogAgeMs(id: string): number | null;
|
|
293
299
|
/** Pure lookup shared by status probes and operator catalog refreshes. */
|
package/dist/registry.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { wrapViewCredentials } from "./runtime/call.js";
|
|
2
|
+
import { META_ROUTES } from "./agent-routes.js";
|
|
3
|
+
import { sha256Hex } from "./bytes.js";
|
|
1
4
|
import { catalogRevision } from "./catalog-revision.js";
|
|
2
5
|
import { authCall, authInvoke } from "./auth/native.js";
|
|
3
6
|
import { Clock, Duration, Effect, Random } from "effect";
|
|
@@ -18,12 +21,16 @@ import { boundedStatus, failureRecord, failureStatus, logFailure, ownStatus } fr
|
|
|
18
21
|
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
19
22
|
import { ContinuationBinding, OperationCredentials, ContinuationStorage, operationView, setViewService, duringViewEffect, } from "./runtime/call.js";
|
|
20
23
|
import { hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
|
|
24
|
+
import { observeStorageWrites } from "./runtime/request-reads.js";
|
|
25
|
+
import { attachStorageDigestKey } from "./storage-digest.js";
|
|
26
|
+
import { requestReads } from "./runtime/request-scope.js";
|
|
27
|
+
import { credentialReads } from "./credential-reads.js";
|
|
21
28
|
import { provideRegistryPrograms, registryEdgeProgram, registryProgram } from "./runtime/registry.js";
|
|
22
29
|
import { causeError, runEdge } from "./runtime/run.js";
|
|
23
30
|
import { Storage } from "./runtime/services.js";
|
|
24
31
|
import { SharedRead } from "./runtime/shared-read.js";
|
|
25
|
-
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet, storageList, } from "./runtime/storage.js";
|
|
26
|
-
import { redactCatalog, sentSecretsFor, sentSecretsForRequest
|
|
32
|
+
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageGetMany, storageSet, storageList, } from "./runtime/storage.js";
|
|
33
|
+
import { redactCatalog, sentSecretsFor, sentSecretsForRequest } from "./sent-secrets.js";
|
|
27
34
|
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
|
|
28
35
|
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashExpiryKeys, stashLedgerKeys, } from "./storage/keys.js";
|
|
29
36
|
import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
|
|
@@ -103,12 +110,14 @@ function namespaced(storage, prefix) {
|
|
|
103
110
|
list: async (keyPrefix) => (await storage.list(prefix + keyPrefix)).map((key) => key.slice(prefix.length)),
|
|
104
111
|
compareAndSet: (k, expected, next, o) => storage.compareAndSet(prefix + k, expected, next, o),
|
|
105
112
|
};
|
|
113
|
+
observeStorageWrites(edge, { storage, prefix });
|
|
106
114
|
return hasNativeStorage(storage)
|
|
107
115
|
? registerNativeStorage(edge, {
|
|
108
116
|
// Select against the underlying current methods at dispatch. A
|
|
109
117
|
// namespace created during a decoration regains the native path when
|
|
110
118
|
// that decoration is restored, without capturing stale capabilities.
|
|
111
119
|
get: (key) => storageGet(prefix + key).pipe(Effect.provideService(Storage, storage)),
|
|
120
|
+
getMany: (keys) => storageGetMany(keys.map((key) => prefix + key)).pipe(Effect.provideService(Storage, storage)),
|
|
112
121
|
set: (key, value, options) => storageSet(prefix + key, value, options).pipe(Effect.provideService(Storage, storage)),
|
|
113
122
|
delete: (key) => storageDelete(prefix + key).pipe(Effect.provideService(Storage, storage)),
|
|
114
123
|
list: (keyPrefix) => storageList(prefix + keyPrefix).pipe(Effect.provideService(Storage, storage), Effect.map((keys) => keys.map((key) => key.slice(prefix.length)))),
|
|
@@ -232,10 +241,6 @@ function swapStashLedger(plan) {
|
|
|
232
241
|
}).pipe(Effect.catch(() => Effect.succeed({ status: "exhausted" })));
|
|
233
242
|
});
|
|
234
243
|
}
|
|
235
|
-
async function sha256Hex(value) {
|
|
236
|
-
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
237
|
-
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
238
|
-
}
|
|
239
244
|
/**
|
|
240
245
|
* Holds the connector set, resolves addresses, and coalesces tool listings
|
|
241
246
|
* within one request. Remote MCP clients own persisted response caching. Connector failures are isolated: a broken
|
|
@@ -461,8 +466,12 @@ export class Registry {
|
|
|
461
466
|
let credentialProgram;
|
|
462
467
|
if (this.opts.credentialVault && credentialConfig) {
|
|
463
468
|
const vault = this.opts.credentialVault;
|
|
469
|
+
// The credential a call sends: always read fresh. It records what it
|
|
470
|
+
// saw in the request's memo, so a later status or detail read in the
|
|
471
|
+
// same request is never older than what was sent.
|
|
472
|
+
const fresh = credentialReads(vault, requestReads(requestScope), true);
|
|
464
473
|
credentialProgram = Effect.tryPromise({
|
|
465
|
-
try: () =>
|
|
474
|
+
try: () => fresh.getAll(id, this.opts.credentialOwner),
|
|
466
475
|
catch: (error) => error,
|
|
467
476
|
}).pipe(Effect.flatMap((values) => {
|
|
468
477
|
const shape = storedCredentialShape(credentialConfig, values);
|
|
@@ -487,6 +496,7 @@ export class Registry {
|
|
|
487
496
|
...(this.opts.oauthClientName !== undefined ? { oauthClientName: this.opts.oauthClientName } : {}),
|
|
488
497
|
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
489
498
|
requestScope,
|
|
499
|
+
routes: this.opts.routes ?? META_ROUTES,
|
|
490
500
|
...callOptions,
|
|
491
501
|
}, scope?.caller);
|
|
492
502
|
setViewService(context, OperationCredentials, credentialProgram);
|
|
@@ -502,7 +512,11 @@ export class Registry {
|
|
|
502
512
|
storage: this.opts.catalogStorage ?? this.opts.storage,
|
|
503
513
|
partition: this.catalogPartition(scope),
|
|
504
514
|
credentialRevision: () => catalogRevision(this.opts.credentialVault),
|
|
505
|
-
sharedPartition: JSON.stringify([
|
|
515
|
+
sharedPartition: JSON.stringify([
|
|
516
|
+
baseUrl,
|
|
517
|
+
this.opts.publicUrl ?? null,
|
|
518
|
+
(scope?.caller ?? scope?.operatorCaller)?.pool ?? null,
|
|
519
|
+
]),
|
|
506
520
|
defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
|
|
507
521
|
minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
|
|
508
522
|
maxTtlMs: (this.opts.catalogMaxTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMaxTtlSeconds) * 1000,
|
|
@@ -517,8 +531,9 @@ export class Registry {
|
|
|
517
531
|
}
|
|
518
532
|
: {}),
|
|
519
533
|
});
|
|
534
|
+
attachStorageDigestKey(context, this.opts.credentialVault);
|
|
520
535
|
sentSecretsFor(context);
|
|
521
|
-
|
|
536
|
+
wrapViewCredentials(context);
|
|
522
537
|
attachOAuthPartition(context, this.oauthPartition);
|
|
523
538
|
// Downstream OAuth state is sealed under the vault key, bound to this
|
|
524
539
|
// connector and owner. The sealer rides beside the context, not on it.
|
|
@@ -581,7 +596,7 @@ export class Registry {
|
|
|
581
596
|
*
|
|
582
597
|
* `chunks[n]` lands on `resultKeys.chunk(id, n)` inside `partition` — the
|
|
583
598
|
* layout connecta.result reads back, so a page fetches only the chunks it covers
|
|
584
|
-
* instead of the whole stored result
|
|
599
|
+
* instead of the whole stored result. Chunking is a read-cost
|
|
585
600
|
* decision, not a capacity one: however many keys an envelope occupies, it
|
|
586
601
|
* is one stash entry charged its total ASCII length.
|
|
587
602
|
*
|
|
@@ -603,7 +618,7 @@ export class Registry {
|
|
|
603
618
|
* connector's namespace. Backs connecta.result.
|
|
604
619
|
*/
|
|
605
620
|
resultIdentity() {
|
|
606
|
-
return { subject: null, principal: null, endpoint: "/mcp", origin: null };
|
|
621
|
+
return { pool: null, subject: null, principal: null, endpoint: "/mcp", origin: null };
|
|
607
622
|
}
|
|
608
623
|
recheckResultAccess(_dependencies, _classification, _signal) {
|
|
609
624
|
return Promise.resolve(true);
|
|
@@ -765,8 +780,8 @@ export class Registry {
|
|
|
765
780
|
getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
766
781
|
return runEdge(registryEdgeProgram(Registry.prototype, this, "getTools", id, baseUrl, requestScope, callOptions, scope));
|
|
767
782
|
}
|
|
768
|
-
credentialDriftFor(id) {
|
|
769
|
-
return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id));
|
|
783
|
+
credentialDriftFor(id, requestScope) {
|
|
784
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id, requestScope));
|
|
770
785
|
}
|
|
771
786
|
/** Age of a complete cache entry; reads never refresh it. */
|
|
772
787
|
catalogAgeMs(id) {
|
|
@@ -946,13 +961,16 @@ export class Registry {
|
|
|
946
961
|
return yield* load.join(signal);
|
|
947
962
|
}));
|
|
948
963
|
}),
|
|
949
|
-
credentialDriftFor: (self, id) => Effect.gen(function* () {
|
|
964
|
+
credentialDriftFor: (self, id, requestScope) => Effect.gen(function* () {
|
|
950
965
|
const credential = self.connectors.get(id)?.credential;
|
|
951
966
|
const vault = self.opts.credentialVault;
|
|
952
967
|
if (!credential || !vault)
|
|
953
968
|
return undefined;
|
|
969
|
+
// Memoized per request: drift only labels a status or detail view.
|
|
970
|
+
// A dispatch reads its credential fresh (contextFor).
|
|
971
|
+
const reader = requestScope ? credentialReads(vault, requestReads(requestScope)) : vault;
|
|
954
972
|
return yield* Effect.tryPromise({
|
|
955
|
-
try: () =>
|
|
973
|
+
try: () => reader.getAll(id, self.opts.credentialOwner),
|
|
956
974
|
catch: (error) => error,
|
|
957
975
|
}).pipe(Effect.map((values) => {
|
|
958
976
|
const shape = storedCredentialShape(credential, values);
|
|
@@ -970,8 +988,13 @@ export class Registry {
|
|
|
970
988
|
const withObservations = (status) => self.withCatalogObservations(id, baseUrl, status, scope);
|
|
971
989
|
return duringViewEffect(ctx, () => Effect.gen(function* () {
|
|
972
990
|
if (connector.credential) {
|
|
991
|
+
// Memoized per request: this read only picks the status shown.
|
|
992
|
+
// The probe that follows reads the credential it sends fresh.
|
|
993
|
+
const vault = self.opts.credentialVault;
|
|
973
994
|
const values = yield* Effect.tryPromise({
|
|
974
|
-
try: () =>
|
|
995
|
+
try: () => vault
|
|
996
|
+
? credentialReads(vault, requestReads(requestScope)).getAll(id, self.opts.credentialOwner)
|
|
997
|
+
: Promise.resolve(null),
|
|
975
998
|
catch: (error) => error,
|
|
976
999
|
});
|
|
977
1000
|
const shape = storedCredentialShape(connector.credential, values);
|
|
@@ -1082,6 +1105,7 @@ class ScopedRegistryView {
|
|
|
1082
1105
|
return {
|
|
1083
1106
|
subject: this.scope.subjectKey ?? null,
|
|
1084
1107
|
principal: this.scope.principalKey ?? null,
|
|
1108
|
+
pool: this.scope.caller?.pool ?? null,
|
|
1085
1109
|
endpoint: this.scope.endpoint ?? "/mcp",
|
|
1086
1110
|
origin: this.scope.origin ?? null,
|
|
1087
1111
|
};
|
|
@@ -1092,8 +1116,8 @@ class ScopedRegistryView {
|
|
|
1092
1116
|
resultsStorage() {
|
|
1093
1117
|
return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
|
|
1094
1118
|
}
|
|
1095
|
-
credentialDriftFor(id) {
|
|
1096
|
-
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id));
|
|
1119
|
+
credentialDriftFor(id, requestScope) {
|
|
1120
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id, requestScope));
|
|
1097
1121
|
}
|
|
1098
1122
|
observedOutputSchema(connectorId, definition) {
|
|
1099
1123
|
return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
|
|
@@ -1149,9 +1173,11 @@ class ScopedRegistryView {
|
|
|
1149
1173
|
}
|
|
1150
1174
|
return carryCatalogFreshness(tools, visible);
|
|
1151
1175
|
}),
|
|
1152
|
-
credentialDriftFor: (self, id) => Effect.suspend(() => {
|
|
1176
|
+
credentialDriftFor: (self, id, requestScope) => Effect.suspend(() => {
|
|
1153
1177
|
const registry = self.registryFor(id);
|
|
1154
|
-
return registry
|
|
1178
|
+
return registry
|
|
1179
|
+
? registryProgram(registry, "credentialDriftFor", id, requestScope)
|
|
1180
|
+
: Effect.succeed(undefined);
|
|
1155
1181
|
}),
|
|
1156
1182
|
statusFor: (self, ...args) => Effect.suspend(() => {
|
|
1157
1183
|
const registry = self.registryFor(args[0]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The admitted principal a request-auth resolver may receive
|
|
1
|
+
// The admitted principal a request-auth resolver may receive.
|
|
2
2
|
//
|
|
3
3
|
// Only the caller core attached from the admitting authorization counts
|
|
4
4
|
// (`callerOf`, see connector-caller.ts). No argument, program or header can
|
package/dist/request-state.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sha256Hex } from "./bytes.js";
|
|
1
2
|
import { ProtocolError, INVALID_PARAMS } from "@modelcontextprotocol/server";
|
|
2
3
|
export const REQUEST_STATE_TTL_MS = 10 * 60_000;
|
|
3
4
|
export const MAX_INPUT_ROUNDS = 3;
|
|
@@ -16,6 +17,5 @@ export function canonicalState(value) {
|
|
|
16
17
|
: item);
|
|
17
18
|
}
|
|
18
19
|
export async function requestDigest(tool, args) {
|
|
19
|
-
|
|
20
|
-
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20
|
+
return sha256Hex(canonicalState({ tool, args }));
|
|
21
21
|
}
|
|
@@ -67,7 +67,7 @@ export async function routeAccessTokens(context, manager) {
|
|
|
67
67
|
const admin = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "access token management", context.runtimeContext, opts.config.logger);
|
|
68
68
|
if (!admin.ok)
|
|
69
69
|
return admin.response;
|
|
70
|
-
// Interactive maintainers alone manage agent tokens
|
|
70
|
+
// Interactive maintainers alone manage agent tokens; anyone else
|
|
71
71
|
// gets the same private 404 every operator route gives them.
|
|
72
72
|
if (!managesAgentTokens(opts, admin) || !admin.identity.principal) {
|
|
73
73
|
return privateJson({ error: "not found" }, { status: 404 });
|
|
@@ -6,7 +6,7 @@ function activityToolVisible(access, event) {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* One disclosure rule for the timeline and the UI's last-call overlay. Only
|
|
9
|
-
* maintainers read activity
|
|
9
|
+
* maintainers read activity, and only after the optional
|
|
10
10
|
* read gate. They may see other principals' shared-connector history,
|
|
11
11
|
* including request IDs, client name/version, and actor kind/id/namespace, on
|
|
12
12
|
* plain /mcp and in every declared pool, within the tools that pool declares.
|
package/dist/routes/connect.js
CHANGED
|
@@ -88,7 +88,7 @@ async function connect(context) {
|
|
|
88
88
|
if (!authz.ok) {
|
|
89
89
|
// Access sign-in is enforced at the edge. Clerk needs its own sign-in page.
|
|
90
90
|
// The sign-in page returns to this link, which then sends a maintainer on
|
|
91
|
-
// to the operator UI. Pool members cannot use that UI yet
|
|
91
|
+
// to the operator UI. Pool members cannot use that UI yet, so
|
|
92
92
|
// signing in there would strand them.
|
|
93
93
|
if (authz.response.status === 401 &&
|
|
94
94
|
authorizationCredential(request).kind === "absent" &&
|
|
@@ -107,7 +107,7 @@ async function connect(context) {
|
|
|
107
107
|
if (authz.principalKey !== handoff.principal || !mayManageConnector(opts, authz, connector)) {
|
|
108
108
|
return refuse("Sign in as the user who requested this connection and has permission to manage it.");
|
|
109
109
|
}
|
|
110
|
-
// The operator UI serves maintainers only until member views ship
|
|
110
|
+
// The operator UI serves maintainers only until member views ship.
|
|
111
111
|
// A member's OAuth link starts here directly, as it does without a UI.
|
|
112
112
|
const viaUi = Boolean(opts.config.ui) && admitsMaintainer(opts, authz);
|
|
113
113
|
if (!connector.startAuth) {
|