@zackbart/connecta 0.34.1 → 0.36.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 +418 -0
- package/README.md +65 -105
- package/SECURITY.md +3 -2
- package/bin/connecta.mjs +41 -16
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +2 -1
- package/dist/access-tokens.js +66 -30
- package/dist/activity.d.ts +1 -1
- package/dist/activity.js +2 -0
- package/dist/agent-config.d.ts +23 -0
- package/dist/agent-config.js +59 -0
- package/dist/agent-description.js +16 -0
- package/dist/agent-identity.js +4 -4
- package/dist/agent-observation.js +39 -0
- package/dist/auth/clerk-sdk/client.d.ts +2 -0
- package/dist/auth/clerk-sdk/generated.js +554 -563
- package/dist/auth/clerk-transport.js +70 -3
- package/dist/auth/clerk.d.ts +13 -0
- package/dist/auth/clerk.js +653 -246
- package/dist/auth/cloudflare-access.js +46 -42
- package/dist/auth/downstream-client-metadata.js +2 -2
- package/dist/auth/downstream-oauth.js +2044 -1161
- package/dist/auth/downstream-revocation.js +88 -88
- package/dist/auth/native-call.js +20 -0
- package/dist/auth/native.js +184 -0
- package/dist/auth/oauth-start-reset.js +11 -5
- package/dist/auth/oauth-token-response.js +121 -102
- package/dist/auth/oauth-v2-migration.js +97 -78
- package/dist/auth/renewable-grant.js +10 -0
- package/dist/auth/static-oauth.js +751 -281
- package/dist/auth/vercel-agent.js +266 -0
- package/dist/auth-admission.js +204 -0
- package/dist/auth-elicitation.js +350 -241
- package/dist/authorization-handoff.js +164 -90
- package/dist/call-admission-context.js +3 -10
- package/dist/call-admission.js +2 -0
- package/dist/call-classification.js +9 -0
- package/dist/call-recovery.js +32 -13
- package/dist/canonical-json.js +8 -0
- package/dist/catalog-cache.js +369 -213
- package/dist/catalog-service.js +659 -530
- package/dist/catalog.js +5 -1
- package/dist/concurrency.js +0 -25
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +3 -1
- package/dist/config-schema.js +13 -3
- package/dist/config.d.ts +40 -6
- package/dist/config.js +40 -26
- package/dist/connector-caller.d.ts +4 -0
- package/dist/connector-caller.js +17 -4
- package/dist/connectors/api-connector.d.ts +29 -7
- package/dist/connectors/api-connector.js +145 -57
- package/dist/connectors/guarded-fetch.d.ts +51 -17
- package/dist/connectors/guarded-fetch.js +215 -121
- package/dist/connectors/negotiation-cache.js +20 -10
- package/dist/connectors/remote-mcp.d.ts +10 -30
- package/dist/connectors/remote-mcp.js +955 -563
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +3 -1
- package/dist/describe-config.js +2 -0
- package/dist/downstream-input.js +347 -260
- package/dist/errors.d.ts +87 -6
- package/dist/errors.js +114 -4
- package/dist/execute.js +87 -50
- package/dist/executor-admission.js +3 -0
- package/dist/executors/quickjs.js +163 -112
- package/dist/guarded-transport.js +50 -0
- package/dist/guest-runtime.js +1 -1
- package/dist/http/client.js +439 -0
- package/dist/http/errors.js +558 -0
- package/dist/http/fields.js +49 -0
- package/dist/http/hatch-paging.js +318 -0
- package/dist/http/hatch.js +1279 -0
- package/dist/http/json-schema.js +267 -0
- package/dist/http/node-transport.js +241 -0
- package/dist/http/oauth.js +162 -0
- package/dist/http/paging.js +227 -0
- package/dist/http/provider.js +221 -0
- package/dist/http/safety.js +82 -0
- package/dist/http/services.js +79 -0
- package/dist/http/tools.js +379 -0
- package/dist/http/vendor-index.js +138 -0
- package/dist/http/vendor.js +448 -0
- package/dist/http/write-request.js +66 -0
- package/dist/http-policy.d.ts +11 -0
- package/dist/http-policy.js +45 -0
- package/dist/inbound-credential.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -0
- package/dist/invocation-auth.js +30 -19
- package/dist/invocation.js +218 -85
- package/dist/mcp-result.js +0 -4
- package/dist/membership.d.ts +4 -0
- package/dist/meta-output.js +1 -1
- package/dist/meta-tools.js +531 -406
- package/dist/native-recovery.js +52 -0
- package/dist/native-tools.js +382 -0
- package/dist/node.d.ts +4 -3
- package/dist/node.js +45 -8
- package/dist/oauth-handoff.js +177 -104
- package/dist/oauth-partition.js +6 -20
- package/dist/oauth-sealing.js +6 -7
- package/dist/operation-vocabulary.js +101 -0
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +17 -4
- package/dist/operator-ui/contract.d.ts +8 -3
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/program-writes.js +47 -26
- package/dist/provider-call-facts.d.ts +115 -0
- package/dist/provider-call-facts.js +182 -0
- package/dist/provider-call.d.ts +112 -0
- package/dist/provider-call.js +271 -0
- package/dist/provider.d.ts +65 -31
- package/dist/provider.js +116 -39
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +427 -0
- package/dist/providers/_shared/google/result-size.js +15 -2
- package/dist/providers/_shared/google/service-account.d.ts +2 -1
- package/dist/providers/_shared/google/service-account.js +8 -4
- package/dist/providers/_shared/google/workspace.d.ts +6 -210
- package/dist/providers/_shared/google/workspace.js +6 -425
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/kit.js +146 -0
- package/dist/providers/_shared/rest/operation-index.js +230 -14
- package/dist/providers/_shared/rest/response-media.js +11 -0
- package/dist/providers/_shared/rest/value-safety.js +353 -195
- package/dist/providers/_shared/sensitive-fields.js +27 -0
- package/dist/providers/breeze/api.js +584 -0
- package/dist/providers/breeze/index.d.ts +1 -3
- package/dist/providers/breeze/index.js +67 -1420
- package/dist/providers/breeze/openapi.generated.js +60 -0
- package/dist/providers/breeze/skill.generated.js +2 -6
- package/dist/providers/breeze/tools.js +696 -0
- package/dist/providers/ccb/api.js +496 -0
- package/dist/providers/ccb/index.d.ts +15 -37
- package/dist/providers/ccb/index.js +77 -1702
- package/dist/providers/ccb/lib/constants.d.ts +34 -0
- package/dist/providers/ccb/lib/constants.js +74 -0
- package/dist/providers/ccb/lib/guide.js +9 -0
- package/dist/providers/ccb/lib/verdicts.js +387 -0
- package/dist/providers/ccb/openapi.generated.js +397 -0
- package/dist/providers/ccb/safety.js +295 -0
- package/dist/providers/ccb/skill.generated.js +2 -6
- package/dist/providers/ccb/tools.js +853 -0
- package/dist/providers/cloudflare/api.js +1066 -0
- package/dist/providers/cloudflare/index.d.ts +5 -22
- package/dist/providers/cloudflare/index.js +79 -197
- package/dist/providers/cloudflare/lib/config.d.ts +16 -0
- package/dist/providers/cloudflare/lib/config.js +2 -0
- package/dist/providers/cloudflare/lib/discovery.js +29 -0
- package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
- package/dist/providers/cloudflare/lib/hatch.js +103 -0
- package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
- package/dist/providers/cloudflare/lib/response-media.js +75 -0
- package/dist/providers/cloudflare/openapi.generated.js +4 -3
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -10
- package/dist/providers/cloudflare/tools.js +360 -0
- package/dist/providers/docs/api.js +51 -0
- package/dist/providers/docs/index.d.ts +1 -12
- package/dist/providers/docs/index.js +53 -1165
- package/dist/providers/docs/lib/constants.d.ts +18 -0
- package/dist/providers/docs/lib/constants.js +18 -0
- package/dist/providers/docs/lib/discovery.js +26 -0
- package/dist/providers/docs/lib/requests.js +48 -0
- package/dist/providers/docs/lib/schemas.js +168 -0
- package/dist/providers/docs/openapi.generated.js +16 -0
- package/dist/providers/docs/safety.js +54 -0
- package/dist/providers/docs/skill.generated.js +2 -6
- package/dist/providers/docs/tools.js +801 -0
- package/dist/providers/drive/api.js +177 -0
- package/dist/providers/drive/index.d.ts +1 -12
- package/dist/providers/drive/index.js +38 -1448
- package/dist/providers/drive/lib/constants.d.ts +6 -0
- package/dist/providers/drive/lib/constants.js +6 -0
- package/dist/providers/drive/lib/contracts.js +532 -0
- package/dist/providers/drive/lib/hatch.js +197 -0
- package/dist/providers/drive/openapi.generated.js +77 -0
- package/dist/providers/drive/safety.js +90 -0
- package/dist/providers/drive/skill.generated.js +2 -6
- package/dist/providers/drive/tools.js +1290 -0
- package/dist/providers/forms/api.js +71 -0
- package/dist/providers/forms/index.d.ts +1 -12
- package/dist/providers/forms/index.js +49 -1145
- package/dist/providers/forms/lib/constants.d.ts +14 -0
- package/dist/providers/forms/lib/constants.js +19 -0
- package/dist/providers/forms/lib/hatch.js +72 -0
- package/dist/providers/forms/openapi.generated.js +23 -0
- package/dist/providers/forms/safety.js +34 -0
- package/dist/providers/forms/skill.generated.js +2 -8
- package/dist/providers/forms/tools.js +1023 -0
- package/dist/providers/github/api.js +150 -0
- package/dist/providers/github/index.d.ts +4 -5
- package/dist/providers/github/index.js +30 -87
- package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
- package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
- package/dist/providers/github/lib/discovery.js +132 -0
- package/dist/providers/github/lib/operations.js +58 -0
- package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
- package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
- package/dist/providers/github/openapi.generated.js +1216 -0
- package/dist/providers/github/safety.js +216 -0
- package/dist/providers/github/skill.generated.js +2 -5
- package/dist/providers/github/tools.js +506 -0
- package/dist/providers/gmail/api.js +114 -0
- package/dist/providers/gmail/index.d.ts +1 -12
- package/dist/providers/gmail/index.js +73 -1621
- package/dist/providers/gmail/lib/constants.d.ts +13 -0
- package/dist/providers/gmail/lib/constants.js +16 -0
- package/dist/providers/gmail/lib/index.js +173 -0
- package/dist/providers/gmail/lib/schemas.js +176 -0
- package/dist/providers/gmail/lib/threading.js +120 -0
- package/dist/providers/gmail/openapi.generated.js +92 -0
- package/dist/providers/gmail/safety.js +21 -0
- package/dist/providers/gmail/skill.generated.js +2 -6
- package/dist/providers/gmail/tools.js +1435 -0
- package/dist/providers/overflow/api.js +1008 -0
- package/dist/providers/overflow/index.d.ts +1 -17
- package/dist/providers/overflow/index.js +60 -1488
- package/dist/providers/overflow/skill.generated.js +2 -5
- package/dist/providers/overflow/tools.js +873 -0
- package/dist/providers/planning-center/api.js +467 -0
- package/dist/providers/planning-center/index.d.ts +4 -35
- package/dist/providers/planning-center/index.js +69 -2445
- package/dist/providers/planning-center/lib/constants.d.ts +58 -0
- package/dist/providers/planning-center/lib/constants.js +83 -0
- package/dist/providers/planning-center/lib/discovery.js +40 -0
- package/dist/providers/planning-center/lib/guide.js +39 -0
- package/dist/providers/planning-center/lib/json-api.js +83 -0
- package/dist/providers/planning-center/lib/verdicts.js +1758 -0
- package/dist/providers/planning-center/openapi.generated.js +1764 -0
- package/dist/providers/planning-center/safety.js +188 -0
- package/dist/providers/planning-center/skill.generated.js +2 -27
- package/dist/providers/planning-center/tools.js +1010 -0
- package/dist/providers/quickbooks/api.js +299 -0
- package/dist/providers/quickbooks/index.d.ts +13 -0
- package/dist/providers/quickbooks/index.js +93 -0
- package/dist/providers/quickbooks/lib/query.js +118 -0
- package/dist/providers/quickbooks/lib/writes.js +168 -0
- package/dist/providers/quickbooks/safety.js +222 -0
- package/dist/providers/quickbooks/skill.generated.js +5 -0
- package/dist/providers/quickbooks/tools.js +117 -0
- package/dist/providers/sheets/api.js +133 -0
- package/dist/providers/sheets/index.d.ts +4 -16
- package/dist/providers/sheets/index.js +61 -1172
- package/dist/providers/sheets/lib/constants.d.ts +14 -0
- package/dist/providers/sheets/lib/constants.js +14 -0
- package/dist/providers/sheets/lib/hatch.js +163 -0
- package/dist/providers/sheets/lib/requests.js +105 -0
- package/dist/providers/sheets/lib/schemas.js +138 -0
- package/dist/providers/sheets/openapi.generated.js +30 -0
- package/dist/providers/sheets/safety.js +27 -0
- package/dist/providers/sheets/skill.generated.js +2 -7
- package/dist/providers/sheets/tools.js +733 -0
- package/dist/providers/slides/api.js +147 -0
- package/dist/providers/slides/index.d.ts +2 -10
- package/dist/providers/slides/index.js +48 -2555
- package/dist/providers/slides/lib/constants.d.ts +11 -0
- package/dist/providers/slides/lib/constants.js +11 -0
- package/dist/providers/slides/lib/requests.js +60 -0
- package/dist/providers/slides/lib/schemas.js +565 -0
- package/dist/providers/slides/openapi.generated.js +18 -0
- package/dist/providers/slides/skill.generated.js +2 -6
- package/dist/providers/slides/tools.js +1968 -0
- package/dist/providers/stripe/api.js +401 -0
- package/dist/providers/stripe/index.d.ts +5 -27
- package/dist/providers/stripe/index.js +74 -210
- package/dist/providers/stripe/lib/config.d.ts +4 -0
- package/dist/providers/stripe/lib/config.js +2 -0
- package/dist/providers/stripe/openapi.generated.js +4 -3
- package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
- package/dist/providers/stripe/skill.generated.js +2 -11
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +17 -0
- package/dist/providers/tithely/index.js +86 -965
- package/dist/providers/tithely/skill.generated.js +2 -5
- package/dist/providers/tithely/tools.js +426 -0
- package/dist/providers/vercel/api.js +551 -0
- package/dist/providers/vercel/index.d.ts +9 -25
- package/dist/providers/vercel/index.js +65 -260
- package/dist/providers/vercel/lib/config.d.ts +2 -0
- package/dist/providers/vercel/lib/config.js +2 -0
- package/dist/providers/vercel/lib/index.js +117 -0
- package/dist/providers/vercel/lib/logs.js +100 -0
- package/dist/providers/vercel/lib/pin.js +270 -0
- package/dist/providers/vercel/openapi.generated.js +10 -5
- package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
- package/dist/providers/vercel/skill.generated.js +2 -7
- package/dist/providers/vercel/tools.js +642 -0
- package/dist/redaction-identity.js +47 -0
- package/dist/redaction-marker.js +125 -0
- package/dist/registry.d.ts +21 -15
- package/dist/registry.js +409 -376
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/actor-labels.js +1 -1
- package/dist/routes/connect.js +1 -1
- package/dist/routes/dispatch.js +103 -0
- package/dist/routes/mcp.js +170 -79
- package/dist/routes/oauth.js +6 -1
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +5 -4
- package/dist/routes/shared.js +244 -177
- package/dist/routes/skills.js +10 -8
- package/dist/routes/ui-config.js +27 -3
- package/dist/routes/ui-data.js +21 -20
- package/dist/routes/ui.js +1 -1
- package/dist/runtime/call-admission.js +0 -26
- package/dist/runtime/call-entry.js +30 -0
- package/dist/runtime/call.js +552 -0
- package/dist/runtime/connector-scope.js +7 -0
- package/dist/runtime/credential-probe.js +34 -0
- package/dist/runtime/executor.js +40 -0
- package/dist/runtime/native-storage.js +63 -0
- package/dist/runtime/registry.js +27 -0
- package/dist/runtime/request-scope.js +72 -0
- package/dist/runtime/run.js +15 -7
- package/dist/runtime/shared-read.js +34 -5
- package/dist/runtime/storage.js +34 -21
- package/dist/sent-secrets.d.ts +194 -37
- package/dist/sent-secrets.js +1743 -340
- package/dist/server.js +5 -56
- package/dist/skills.js +322 -223
- package/dist/sqlite.js +15 -14
- package/dist/storage/d1-copy.js +255 -0
- package/dist/storage/d1-driver.js +15 -0
- package/dist/storage/keys.js +28 -0
- package/dist/storage/sql-driver.js +88 -0
- package/dist/storage/sql-native.js +544 -0
- package/dist/storage/sql-schema.d.ts +6 -0
- package/dist/storage/sql-schema.js +15 -0
- package/dist/storage/sql.d.ts +14 -20
- package/dist/storage/sql.js +29 -535
- package/dist/timeout.js +0 -26
- package/dist/tool-safety.js +20 -10
- package/dist/types.d.ts +9 -109
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +0 -1
- package/dist/upward.js +3 -0
- package/dist/usage-guide.js +3 -1
- package/dist/validate.d.ts +4 -2
- package/dist/validate.js +86 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +312 -292
- package/dist/workspace-subject.js +5 -1
- package/dist/write-outcome.js +454 -34
- package/documentation/README.md +28 -7
- package/documentation/architecture.md +2409 -255
- package/documentation/auth.md +867 -114
- package/documentation/code-mode.md +49 -16
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +129 -144
- package/documentation/meta-tools.md +409 -32
- package/documentation/operating.md +27 -3
- package/documentation/operator-tests.md +3 -0
- package/documentation/operator-ui.md +84 -48
- package/documentation/provider-auth-migration.md +16 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +1347 -0
- package/examples/worker/AGENTS.md +23 -9
- package/examples/worker/README.md +254 -135
- package/examples/worker/alchemy.run.ts +116 -0
- package/examples/worker/gitignore +10 -0
- package/examples/worker/src/connecta.config.ts +23 -21
- package/examples/worker/wrangler.jsonc +14 -5
- package/guides/connecting-agents.md +157 -0
- package/guides/how-it-works.md +156 -0
- package/guides/safety-and-trust.md +153 -0
- package/package.json +30 -50
- package/templates/node/.env.example +3 -0
- package/templates/node/README.md +22 -2
- package/templates/node/docker-compose.yml +1 -0
- package/templates/node/package.json +3 -2
- package/templates/node/src/connecta.config.ts +8 -0
- package/dist/catalog-drift.d.ts +0 -138
- package/dist/catalog-drift.js +0 -578
- package/dist/operator-ui/agents.d.ts +0 -74
- package/dist/operator-ui/agents.js +0 -104
- package/dist/operator-ui/config-summary.js +0 -565
- package/dist/operator-ui/connection-map.js +0 -376
- package/dist/operator-ui/endpoints.js +0 -221
- package/dist/operator-ui/fix-prompts.d.ts +0 -18
- package/dist/operator-ui/fix-prompts.js +0 -87
- package/dist/operator-ui/health.js +0 -52
- package/dist/operator-ui/member-page.js +0 -97
- package/dist/operator-ui/overview.js +0 -123
- package/dist/operator-ui/setup-commands.js +0 -51
- package/dist/operator-ui/view.d.ts +0 -401
- package/dist/operator-ui/view.js +0 -705
- package/dist/providers/_shared/request-token.js +0 -83
- package/dist/providers/_shared/rest/dispatch.js +0 -41
- package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
- package/dist/providers/_shared/rest/tools.d.ts +0 -141
- package/dist/providers/_shared/rest/tools.js +0 -569
- package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -45
- package/dist/providers/basecamp/index.js +0 -997
- package/dist/providers/basecamp/skill.generated.js +0 -9
- package/dist/providers/cloudflare/rest.d.ts +0 -37
- package/dist/providers/cloudflare/rest.js +0 -1097
- package/dist/providers/github/hosted.js +0 -218
- package/dist/providers/github/rest.js +0 -321
- package/dist/providers/infisical/index.d.ts +0 -7
- package/dist/providers/infisical/index.js +0 -713
- package/dist/providers/infisical/skill.generated.js +0 -8
- package/dist/providers/intercom/index.d.ts +0 -24
- package/dist/providers/intercom/index.js +0 -121
- package/dist/providers/intercom/skill.generated.js +0 -8
- package/dist/providers/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -537
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -59
- package/dist/providers/mixpanel/index.js +0 -472
- package/dist/providers/mixpanel/operations.js +0 -20
- package/dist/providers/mixpanel/rest.js +0 -603
- package/dist/providers/mixpanel/skill.generated.js +0 -10
- package/dist/providers/mixpanel/value-safety.js +0 -16
- package/dist/providers/notion/index.d.ts +0 -67
- package/dist/providers/notion/index.js +0 -368
- package/dist/providers/notion/openapi.generated.js +0 -75
- package/dist/providers/notion/rest.d.ts +0 -24
- package/dist/providers/notion/rest.js +0 -1226
- package/dist/providers/notion/skill.generated.js +0 -11
- package/dist/providers/notion/value-safety.js +0 -56
- package/dist/providers/revenuecat/index.d.ts +0 -51
- package/dist/providers/revenuecat/index.js +0 -697
- package/dist/providers/revenuecat/skill.generated.js +0 -11
- package/dist/providers/stripe/rest.d.ts +0 -14
- package/dist/providers/stripe/rest.js +0 -402
- package/dist/providers/vercel/rest.d.ts +0 -20
- package/dist/providers/vercel/rest.js +0 -1020
- package/dist/providers/wispr-flow/index.d.ts +0 -14
- package/dist/providers/wispr-flow/index.js +0 -117
- package/dist/providers/wispr-flow/skill.generated.js +0 -8
- /package/dist/providers/github/{key.js → lib/key.js} +0 -0
package/dist/registry.js
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { authCall, authInvoke } from "./auth/native.js";
|
|
2
|
+
import { Clock, Duration, Effect, Random } from "effect";
|
|
2
3
|
import { activityRequest } from "./activity-request.js";
|
|
3
|
-
import {
|
|
4
|
+
import { aggregateCallAdmissionSnapshots, ConnectorCallAdmissionController, } from "./call-admission.js";
|
|
5
|
+
import { attachCatalogCache, catalogExpiry, catalogFetchedAt, customCatalogFallback, invalidateCatalogCache, observeUncachedCatalogRefresh, storeCustomCatalogFallback, } from "./catalog-cache.js";
|
|
6
|
+
import { carryCatalogFreshness, markCatalogFreshness } from "./catalog-freshness.js";
|
|
7
|
+
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
|
|
4
8
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
9
|
+
import { usesCallerIdentity } from "./connector-caller.js";
|
|
10
|
+
import { canonicalJson } from "./canonical-json.js";
|
|
7
11
|
import { storedCredentialShape } from "./credential-rules.js";
|
|
8
12
|
import { ConnectorCallError } from "./errors.js";
|
|
13
|
+
import { attachOAuthPartition, oauthPartitionIdle } from "./oauth-partition.js";
|
|
14
|
+
import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
|
|
9
15
|
import { boundedStatus, failureRecord, failureStatus, logFailure, ownStatus } from "./operator-record.js";
|
|
10
|
-
import { ConnectorCallAdmissionController, aggregateCallAdmissionSnapshots, } from "./call-admission.js";
|
|
11
|
-
import { boundedCatalogDrift, catalogReviewOf, classifyCatalog, observeReviewedDrift, observedCatalogDrift, } from "./catalog-drift.js";
|
|
12
|
-
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
|
|
13
16
|
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
17
|
+
import { ContinuationBinding, OperationCredentials, ContinuationStorage, operationView, setViewService, duringViewEffect, } from "./runtime/call.js";
|
|
18
|
+
import { hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
|
|
19
|
+
import { provideRegistryPrograms, registryEdgeProgram, registryProgram } from "./runtime/registry.js";
|
|
20
|
+
import { causeError, runEdge } from "./runtime/run.js";
|
|
21
|
+
import { Storage } from "./runtime/services.js";
|
|
22
|
+
import { SharedRead } from "./runtime/shared-read.js";
|
|
23
|
+
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet, storageList, } from "./runtime/storage.js";
|
|
14
24
|
import { redactCatalog, sentSecretsFor, sentSecretsForRequest, trackCredentialReads } from "./sent-secrets.js";
|
|
15
25
|
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
26
|
+
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashExpiryKeys, stashLedgerKeys, } from "./storage/keys.js";
|
|
27
|
+
import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
|
|
28
|
+
import { carryToolClassification, classifyTool } from "./tool-safety.js";
|
|
19
29
|
import { isWorkspaceAddress } from "./workspace-address.js";
|
|
20
30
|
import { workspaceSubjectOf } from "./workspace-subject.js";
|
|
21
|
-
import { runEdge } from "./runtime/run.js";
|
|
22
|
-
import { SharedRead } from "./runtime/shared-read.js";
|
|
23
|
-
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet } from "./runtime/storage.js";
|
|
24
|
-
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, stashExpiryKeys, } from "./storage/keys.js";
|
|
25
|
-
import { classifyTool } from "./tool-safety.js";
|
|
26
31
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
27
32
|
const DEFAULT_MAX_RESULT_BYTES = CONFIG_DEFAULTS.calls.maxResultBytes;
|
|
28
33
|
const encoder = new TextEncoder();
|
|
@@ -74,7 +79,7 @@ export function isValidMaxResultBytes(value) {
|
|
|
74
79
|
export function resolveMaxResultBytes(value, inherited) {
|
|
75
80
|
return value !== undefined && isValidMaxResultBytes(value) ? value : inherited;
|
|
76
81
|
}
|
|
77
|
-
/** Freeze registry-owned facts
|
|
82
|
+
/** Freeze registry-owned catalog facts before caching. */
|
|
78
83
|
function frozenFacts(tools) {
|
|
79
84
|
const pending = [tools];
|
|
80
85
|
while (pending.length > 0) {
|
|
@@ -88,7 +93,7 @@ function frozenFacts(tools) {
|
|
|
88
93
|
return tools;
|
|
89
94
|
}
|
|
90
95
|
function namespaced(storage, prefix) {
|
|
91
|
-
|
|
96
|
+
const edge = {
|
|
92
97
|
capabilities: storage.capabilities,
|
|
93
98
|
get: (k) => storage.get(prefix + k),
|
|
94
99
|
set: (k, v, o) => storage.set(prefix + k, v, o),
|
|
@@ -96,6 +101,18 @@ function namespaced(storage, prefix) {
|
|
|
96
101
|
list: async (keyPrefix) => (await storage.list(prefix + keyPrefix)).map((key) => key.slice(prefix.length)),
|
|
97
102
|
compareAndSet: (k, expected, next, o) => storage.compareAndSet(prefix + k, expected, next, o),
|
|
98
103
|
};
|
|
104
|
+
return hasNativeStorage(storage)
|
|
105
|
+
? registerNativeStorage(edge, {
|
|
106
|
+
// Select against the underlying current methods at dispatch. A
|
|
107
|
+
// namespace created during a decoration regains the native path when
|
|
108
|
+
// that decoration is restored, without capturing stale capabilities.
|
|
109
|
+
get: (key) => storageGet(prefix + key).pipe(Effect.provideService(Storage, storage)),
|
|
110
|
+
set: (key, value, options) => storageSet(prefix + key, value, options).pipe(Effect.provideService(Storage, storage)),
|
|
111
|
+
delete: (key) => storageDelete(prefix + key).pipe(Effect.provideService(Storage, storage)),
|
|
112
|
+
list: (keyPrefix) => storageList(prefix + keyPrefix).pipe(Effect.provideService(Storage, storage), Effect.map((keys) => keys.map((key) => key.slice(prefix.length)))),
|
|
113
|
+
compareAndSet: (key, expected, next, options) => storageCompareAndSet(prefix + key, expected, next, options).pipe(Effect.provideService(Storage, storage)),
|
|
114
|
+
})
|
|
115
|
+
: edge;
|
|
99
116
|
}
|
|
100
117
|
const MAX_PERSONAL_REGISTRIES = 1_024;
|
|
101
118
|
const MAX_ABSENT_GRANT_WARNINGS = 1_024;
|
|
@@ -231,6 +248,7 @@ export class Registry {
|
|
|
231
248
|
catalogAccess = new Map();
|
|
232
249
|
/** Bounded tokens isolating count-only observations by catalog identity. */
|
|
233
250
|
catalogObservationPartitions = new Map();
|
|
251
|
+
overrideWarningCatalogs = new WeakMap();
|
|
234
252
|
observedOutputSchemas;
|
|
235
253
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
236
254
|
maxResultBytes;
|
|
@@ -271,11 +289,6 @@ export class Registry {
|
|
|
271
289
|
`normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
|
|
272
290
|
"Shorten it or omit it to derive one.");
|
|
273
291
|
}
|
|
274
|
-
// Validated here, before any request (INV-11), and read once per
|
|
275
|
-
// connector object.
|
|
276
|
-
if (catalogReviewOf(c) && c.staticTools) {
|
|
277
|
-
throw new Error(`Connector "${c.id}" declares both staticTools and a classification; ` + "annotate static tools directly.");
|
|
278
|
-
}
|
|
279
292
|
this.connectors.set(c.id, c);
|
|
280
293
|
if (c.callAdmission !== undefined) {
|
|
281
294
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
@@ -288,8 +301,8 @@ export class Registry {
|
|
|
288
301
|
Object.freeze(this.classification);
|
|
289
302
|
for (const [id, overrides] of Object.entries(this.classification)) {
|
|
290
303
|
for (const [name, verdict] of Object.entries(overrides)) {
|
|
291
|
-
if (!name || (verdict !== "read" && verdict !== "write")) {
|
|
292
|
-
throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read" or "
|
|
304
|
+
if (!name || (verdict !== "read" && verdict !== "write" && verdict !== "hidden")) {
|
|
305
|
+
throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read", "write", or "hidden"`);
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
308
|
const connector = this.connectors.get(id);
|
|
@@ -326,6 +339,7 @@ export class Registry {
|
|
|
326
339
|
...this.opts,
|
|
327
340
|
classification: Object.fromEntries(Object.entries(this.classification).filter(([id]) => this.connectors.get(id)?.authScope === "personal")),
|
|
328
341
|
storage: namespaced(this.opts.storage, scopes.principal(principalKey)),
|
|
342
|
+
rootStorage: this.opts.rootStorage ?? this.opts.storage,
|
|
329
343
|
credentialOwner: principalKey,
|
|
330
344
|
catalogStorage: this.opts.catalogStorage ?? this.opts.storage,
|
|
331
345
|
constructionChecks: false,
|
|
@@ -373,45 +387,14 @@ export class Registry {
|
|
|
373
387
|
const quoted = JSON.stringify(key).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
374
388
|
this.opts.logger.warn(`a pool grants ${quoted} but connector "${connectorId}" lists no such tool; the grant is unreachable`);
|
|
375
389
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
for (let attempt = 0; attempt < 32; attempt++) {
|
|
379
|
-
const existing = await this.opts.storage.get(key);
|
|
380
|
-
if (existing && existing !== principalKey) {
|
|
381
|
-
throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
|
|
382
|
-
}
|
|
383
|
-
// Bind ownership atomically, including renewal of the same owner's
|
|
384
|
-
// handoff. Two owners reading a miss must not overwrite each other.
|
|
385
|
-
if (await this.opts.storage.compareAndSet(key, existing, principalKey, {
|
|
386
|
-
ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS,
|
|
387
|
-
}))
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
throw new Error(`OAuth handoff for "${connectorId}" is busy; retry authorization`);
|
|
390
|
+
storeOAuthHandoff(connectorId, state, principalKey) {
|
|
391
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "storeOAuthHandoff", connectorId, state, principalKey));
|
|
391
392
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return null;
|
|
395
|
-
const principalKey = await this.opts.storage.get(oauthHandoffKeys.handoff(connectorId, await sha256Hex(state)));
|
|
396
|
-
const connector = this.connectors.get(connectorId);
|
|
397
|
-
if (!connector || !principalKey)
|
|
398
|
-
return null;
|
|
399
|
-
return {
|
|
400
|
-
registry: connector.authScope === "personal"
|
|
401
|
-
? this.scoped({
|
|
402
|
-
connectorIds: [connectorId],
|
|
403
|
-
subjectKey: principalKey,
|
|
404
|
-
principalKey,
|
|
405
|
-
})
|
|
406
|
-
: this,
|
|
407
|
-
principalKey,
|
|
408
|
-
};
|
|
393
|
+
oauthCallbackView(connectorId, state) {
|
|
394
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "oauthCallbackView", connectorId, state));
|
|
409
395
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return false;
|
|
413
|
-
const key = oauthHandoffKeys.handoff(connectorId, await sha256Hex(state));
|
|
414
|
-
return this.opts.storage.compareAndSet(key, principalKey, null);
|
|
396
|
+
consumeOAuthHandoff(connectorId, state, principalKey) {
|
|
397
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "consumeOAuthHandoff", connectorId, state, principalKey));
|
|
415
398
|
}
|
|
416
399
|
/**
|
|
417
400
|
* Refuse an unusable result cap at construction, like every other
|
|
@@ -465,18 +448,22 @@ export class Registry {
|
|
|
465
448
|
return this.connectors.has(id);
|
|
466
449
|
}
|
|
467
450
|
contextFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
468
|
-
const
|
|
451
|
+
const connector = this.connectors.get(id);
|
|
452
|
+
const credentialConfig = connector?.credential;
|
|
469
453
|
let credentialAccess;
|
|
454
|
+
let credentialProgram;
|
|
470
455
|
if (this.opts.credentialVault && credentialConfig) {
|
|
471
456
|
const vault = this.opts.credentialVault;
|
|
472
|
-
|
|
473
|
-
|
|
457
|
+
credentialProgram = Effect.tryPromise({
|
|
458
|
+
try: () => vault.getAll(id, this.opts.credentialOwner),
|
|
459
|
+
catch: (error) => error,
|
|
460
|
+
}).pipe(Effect.flatMap((values) => {
|
|
474
461
|
const shape = storedCredentialShape(credentialConfig, values);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
462
|
+
return shape.state === "mismatch"
|
|
463
|
+
? Effect.fail(new ConnectorCallError("auth_required", shape.message))
|
|
464
|
+
: Effect.succeed(values);
|
|
465
|
+
}));
|
|
466
|
+
const readValues = () => runEdge(credentialProgram);
|
|
480
467
|
credentialAccess = {
|
|
481
468
|
get: async (field = "value") => {
|
|
482
469
|
const values = await readValues();
|
|
@@ -485,7 +472,7 @@ export class Registry {
|
|
|
485
472
|
getAll: readValues,
|
|
486
473
|
};
|
|
487
474
|
}
|
|
488
|
-
const context = {
|
|
475
|
+
const context = operationView({
|
|
489
476
|
storage: namespaced(this.opts.storage, scopes.connector(id)),
|
|
490
477
|
logger: this.opts.logger,
|
|
491
478
|
baseUrl,
|
|
@@ -494,8 +481,16 @@ export class Registry {
|
|
|
494
481
|
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
495
482
|
requestScope,
|
|
496
483
|
...callOptions,
|
|
497
|
-
};
|
|
498
|
-
|
|
484
|
+
}, scope?.caller);
|
|
485
|
+
setViewService(context, OperationCredentials, credentialProgram);
|
|
486
|
+
setViewService(context, ContinuationStorage, this.opts.rootStorage ?? this.opts.storage);
|
|
487
|
+
const owner = this.opts.credentialOwner ?? null;
|
|
488
|
+
setViewService(context, ContinuationBinding, {
|
|
489
|
+
connection: id,
|
|
490
|
+
// Include every admitted input a subject mapping can inspect, including
|
|
491
|
+
// the pool owner for an agent. This value enters only the HMAC claims.
|
|
492
|
+
principal: connector && usesCallerIdentity(connector) ? canonicalJson([owner, scope?.caller ?? null]) : owner,
|
|
493
|
+
});
|
|
499
494
|
attachCatalogCache(context, {
|
|
500
495
|
storage: this.opts.catalogStorage ?? this.opts.storage,
|
|
501
496
|
partition: this.catalogPartition(scope),
|
|
@@ -564,17 +559,6 @@ export class Registry {
|
|
|
564
559
|
}
|
|
565
560
|
return partition;
|
|
566
561
|
}
|
|
567
|
-
catalogDriftOf(connector, baseUrl, scope) {
|
|
568
|
-
const partition = this.catalogObservationPartition(connector.id, baseUrl, scope);
|
|
569
|
-
const observed = partition ? observedCatalogDrift(connector, partition) : undefined;
|
|
570
|
-
// Plugin reports cannot establish a reviewed baseline or schema coverage
|
|
571
|
-
// that this registry has not checked. Personal plugin seams have no subject
|
|
572
|
-
// parameter and must never bypass the partition above.
|
|
573
|
-
const fallback = !observed && !catalogReviewOf(connector) && connector.authScope !== "personal"
|
|
574
|
-
? connector.catalogDrift?.()
|
|
575
|
-
: undefined;
|
|
576
|
-
return boundedCatalogDrift(observed ?? (fallback ? { ...fallback, reviewedBaseline: false } : undefined));
|
|
577
|
-
}
|
|
578
562
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
579
563
|
closeCallAdmission() {
|
|
580
564
|
this.callAdmissionClosed = true;
|
|
@@ -602,88 +586,7 @@ export class Registry {
|
|
|
602
586
|
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
603
587
|
*/
|
|
604
588
|
stashResult(id, chunks, ttlSeconds, partition = scopes.results) {
|
|
605
|
-
return runOnPartition(
|
|
606
|
-
const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
|
|
607
|
-
const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
|
|
608
|
-
// The paging envelope is ASCII, so its string length is its stored byte count.
|
|
609
|
-
const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
610
|
-
if (bytes > maxBytes || maxEntries === 0)
|
|
611
|
-
return false;
|
|
612
|
-
const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
|
|
613
|
-
// IDs belong to reservations, not reusable result IDs or subjects.
|
|
614
|
-
const charge = crypto.randomUUID();
|
|
615
|
-
const ttlMs = ttlSeconds * 1000;
|
|
616
|
-
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
617
|
-
return false;
|
|
618
|
-
const booking = yield* swapStashLedger((live, now) => {
|
|
619
|
-
// A committed CAS with a lost response is already our booking, even
|
|
620
|
-
// at full capacity. Reuse its deadline instead of adding a second slot.
|
|
621
|
-
const own = live.find((entry) => entry[0] === charge);
|
|
622
|
-
if (own)
|
|
623
|
-
return { result: own[2] };
|
|
624
|
-
const used = live.reduce((sum, entry) => sum + entry[1], 0);
|
|
625
|
-
if (live.length >= maxEntries || used + bytes > maxBytes)
|
|
626
|
-
return { result: undefined };
|
|
627
|
-
const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
|
|
628
|
-
if (!Number.isSafeInteger(deadline))
|
|
629
|
-
return { result: undefined };
|
|
630
|
-
return { entries: [...live, [charge, bytes, deadline]], result: deadline };
|
|
631
|
-
});
|
|
632
|
-
if (booking.status === "exhausted" || booking.result === undefined)
|
|
633
|
-
return false;
|
|
634
|
-
const deadline = booking.result;
|
|
635
|
-
const writeDeadline = deadline - ttlMs;
|
|
636
|
-
const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
|
|
637
|
-
const owns = (entry) => entry[0] === charge;
|
|
638
|
-
const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
|
|
639
|
-
? {
|
|
640
|
-
entries: live.map((entry) => (owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry)),
|
|
641
|
-
result: undefined,
|
|
642
|
-
}
|
|
643
|
-
: { result: undefined });
|
|
644
|
-
let pendingWrite = false;
|
|
645
|
-
// A rejected or timed-out driver call may still commit after deletion.
|
|
646
|
-
// Only a confirmed write response clears pendingWrite.
|
|
647
|
-
const release = completeStash(Effect.gen(function* () {
|
|
648
|
-
const deleted = yield* Effect.gen(function* () {
|
|
649
|
-
for (const key of keys)
|
|
650
|
-
yield* storageDelete(key);
|
|
651
|
-
return true;
|
|
652
|
-
}).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
653
|
-
if (pendingWrite)
|
|
654
|
-
return;
|
|
655
|
-
if (deleted) {
|
|
656
|
-
const released = yield* swapStashLedger((live) => live.some(owns)
|
|
657
|
-
? { entries: live.filter((entry) => !owns(entry)), result: undefined }
|
|
658
|
-
: { result: undefined });
|
|
659
|
-
if (released.status === "applied")
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
yield* settle;
|
|
663
|
-
}));
|
|
664
|
-
// Trailing chunks first, then the header. The caller receives no result
|
|
665
|
-
// ID after failure or timeout, including a late header commit.
|
|
666
|
-
const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
|
|
667
|
-
const written = yield* Effect.gen(function* () {
|
|
668
|
-
for (let index = keys.length - 1; index >= 0; index--) {
|
|
669
|
-
const before = yield* Clock.currentTimeMillis;
|
|
670
|
-
if (before >= writeDeadline || before >= chunkExpiry)
|
|
671
|
-
return false;
|
|
672
|
-
pendingWrite = true;
|
|
673
|
-
yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
674
|
-
pendingWrite = false;
|
|
675
|
-
})));
|
|
676
|
-
if ((yield* Clock.currentTimeMillis) >= writeDeadline)
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
return true;
|
|
680
|
-
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
|
|
681
|
-
if (!written)
|
|
682
|
-
yield* release;
|
|
683
|
-
else
|
|
684
|
-
yield* completeStash(settle);
|
|
685
|
-
return written;
|
|
686
|
-
}), this.opts);
|
|
589
|
+
return runOnPartition(registryEdgeProgram(Registry.prototype, this, "stashResult", id, chunks, ttlSeconds, partition), this.opts);
|
|
687
590
|
}
|
|
688
591
|
/**
|
|
689
592
|
* Storage namespaced to the root result partition, kept separate from any
|
|
@@ -692,7 +595,7 @@ export class Registry {
|
|
|
692
595
|
resultIdentity() {
|
|
693
596
|
return { subject: null, principal: null, endpoint: "/mcp", origin: null };
|
|
694
597
|
}
|
|
695
|
-
recheckResultAccess(
|
|
598
|
+
recheckResultAccess(_dependencies, _classification, _signal) {
|
|
696
599
|
return Promise.resolve(true);
|
|
697
600
|
}
|
|
698
601
|
resultsStorage() {
|
|
@@ -701,11 +604,8 @@ export class Registry {
|
|
|
701
604
|
credentialUiAvailable() {
|
|
702
605
|
return Boolean(this.opts.credentialUi);
|
|
703
606
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (!state || !principalKey)
|
|
707
|
-
throw new Error("OAuth requires state and an initiating user");
|
|
708
|
-
await this.storeOAuthHandoff(id, state, principalKey);
|
|
607
|
+
bindOAuthHandoff(id, authorizationUrl, principalKey) {
|
|
608
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "bindOAuthHandoff", id, authorizationUrl, principalKey));
|
|
709
609
|
}
|
|
710
610
|
observedOutputSchema(connectorId, definition) {
|
|
711
611
|
return this.observedOutputSchemas.get(connectorId, definition);
|
|
@@ -741,56 +641,43 @@ export class Registry {
|
|
|
741
641
|
...(connector.authScope === "personal" ? { personal: true } : {}),
|
|
742
642
|
}, activityRequest(ctx.requestScope));
|
|
743
643
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (tools.length > MAX_CATALOG_TOOLS ||
|
|
765
|
-
encoder.encode(JSON.stringify(tools)).byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
|
|
766
|
-
throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
|
|
767
|
-
}
|
|
768
|
-
const facts = frozenFacts(structuredClone(tools));
|
|
769
|
-
const accepted = this.acceptToolNames(facts);
|
|
770
|
-
const review = catalogReviewOf(connector);
|
|
771
|
-
const verification = await observeReviewedDrift(connector, review, accepted, this.opts.logger, this.catalogObservationPartition(id, baseUrl, scope, true), facts.length - accepted.length);
|
|
772
|
-
if (catalogFetchedAt(ctx) === undefined) {
|
|
773
|
-
try {
|
|
774
|
-
await storeCustomCatalogFallback(ctx, id, facts);
|
|
775
|
-
}
|
|
776
|
-
catch (error) {
|
|
777
|
-
logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
|
|
778
|
-
}
|
|
779
|
-
try {
|
|
780
|
-
await observeUncachedCatalogRefresh(ctx, id, facts);
|
|
644
|
+
loadDownstreamTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
645
|
+
return Effect.gen({ self: this }, function* () {
|
|
646
|
+
const connector = this.connectors.get(id);
|
|
647
|
+
if (!connector)
|
|
648
|
+
throw new Error(`Unknown connector "${id}"`);
|
|
649
|
+
if (connector.staticTools)
|
|
650
|
+
return markCatalogFreshness(connector.staticTools, Infinity);
|
|
651
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
652
|
+
const listed = yield* authInvoke(connector, "listTools", [ctx]).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
653
|
+
const fallback = error instanceof ConnectorCallError && error.code === "unavailable"
|
|
654
|
+
? yield* authCall(customCatalogFallback, [ctx, id]).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
655
|
+
: undefined;
|
|
656
|
+
return fallback
|
|
657
|
+
? fallback
|
|
658
|
+
: yield* Effect.fail(sentSecretsForRequest(ctx.requestScope ?? ctx).redact(error));
|
|
659
|
+
})));
|
|
660
|
+
const tools = redactCatalog(ctx, listed).map(({ classification: _ignored, ...fact }) => fact);
|
|
661
|
+
if (tools.length > MAX_CATALOG_TOOLS ||
|
|
662
|
+
encoder.encode(JSON.stringify(tools)).byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
|
|
663
|
+
throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
|
|
781
664
|
}
|
|
782
|
-
|
|
783
|
-
|
|
665
|
+
const facts = frozenFacts(structuredClone(tools));
|
|
666
|
+
const accepted = this.acceptToolNames(facts);
|
|
667
|
+
if (catalogFetchedAt(ctx) === undefined) {
|
|
668
|
+
for (const observe of [storeCustomCatalogFallback, observeUncachedCatalogRefresh]) {
|
|
669
|
+
yield* authCall(observe, [ctx, id, facts]).pipe(Effect.catch((error) => Effect.sync(() => logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error)))));
|
|
670
|
+
}
|
|
784
671
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
672
|
+
this.catalogObservedAt.set(id, catalogFetchedAt(ctx) ?? Date.now());
|
|
673
|
+
const provenance = catalogExpiry(ctx);
|
|
674
|
+
const freshUntil = provenance && !provenance.staleFallback ? provenance.expiresAt : 0;
|
|
675
|
+
this.catalogAccess.set(id, {
|
|
676
|
+
state: freshUntil > Date.now() ? "fresh" : "stale",
|
|
677
|
+
observedAt: new Date().toISOString(),
|
|
678
|
+
});
|
|
679
|
+
return markCatalogFreshness(accepted, freshUntil);
|
|
792
680
|
});
|
|
793
|
-
return { tools: markCatalogFreshness(accepted, freshUntil), verification };
|
|
794
681
|
}
|
|
795
682
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
796
683
|
acceptToolNames(tools) {
|
|
@@ -799,30 +686,28 @@ export class Registry {
|
|
|
799
686
|
}
|
|
800
687
|
/**
|
|
801
688
|
* One connector's catalog as served: the cached downstream facts, classified
|
|
802
|
-
* on every read by
|
|
803
|
-
*
|
|
804
|
-
* stale verdict — from an older review, an older release, or a stale
|
|
805
|
-
* fallback — and nothing a caller or decorator does to a served tool reaches
|
|
806
|
-
* the next read.
|
|
689
|
+
* on every read by annotations and deployment overrides, into fresh objects.
|
|
690
|
+
* No cache or caller mutation can carry a stale verdict into the next read.
|
|
807
691
|
*/
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
692
|
+
loadTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
693
|
+
return Effect.gen({ self: this }, function* () {
|
|
694
|
+
const tools = yield* this.loadDownstreamTools(id, baseUrl, requestScope, callOptions, scope);
|
|
695
|
+
const connector = this.connectors.get(id);
|
|
696
|
+
if (!connector)
|
|
697
|
+
throw new Error(`Unknown connector "${id}"`);
|
|
698
|
+
const accepted = this.acceptToolNames(tools);
|
|
699
|
+
const overrides = this.classification[id];
|
|
700
|
+
this.validateClassification(id, accepted, overrides, this.catalogObservationPartition(id, baseUrl, scope, true));
|
|
701
|
+
const visible = accepted.filter((tool) => overrides?.[tool.name] !== "hidden");
|
|
702
|
+
const served = visible.map((tool) => this.publishTool(id, tool));
|
|
703
|
+
return carryCatalogFreshness(tools, served);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
publishTool(id, tool) {
|
|
822
707
|
const override = this.classification[id]?.[tool.name];
|
|
823
|
-
return {
|
|
708
|
+
return carryToolClassification(tool, {
|
|
824
709
|
...structuredClone(tool),
|
|
825
|
-
classification: classifyTool(tool, override),
|
|
710
|
+
classification: classifyTool(tool, override === "hidden" ? undefined : override),
|
|
826
711
|
...(override !== undefined
|
|
827
712
|
? {
|
|
828
713
|
annotations: {
|
|
@@ -832,65 +717,42 @@ export class Registry {
|
|
|
832
717
|
},
|
|
833
718
|
}
|
|
834
719
|
: {}),
|
|
835
|
-
};
|
|
720
|
+
});
|
|
836
721
|
}
|
|
837
722
|
/** Construction-only view for the secret-free static config description. */
|
|
838
723
|
describeStaticTools(id) {
|
|
839
724
|
const tools = this.connectors.get(id)?.staticTools;
|
|
840
|
-
return tools
|
|
725
|
+
return tools
|
|
726
|
+
?.filter((tool) => this.classification[id]?.[tool.name] !== "hidden")
|
|
727
|
+
.map((tool) => this.publishTool(id, tool));
|
|
841
728
|
}
|
|
842
|
-
validateClassification(id, tools, overrides) {
|
|
729
|
+
validateClassification(id, tools, overrides, partition) {
|
|
730
|
+
if (!overrides)
|
|
731
|
+
return;
|
|
732
|
+
if (partition) {
|
|
733
|
+
const catalog = JSON.stringify(tools);
|
|
734
|
+
if (this.overrideWarningCatalogs.get(partition) === catalog)
|
|
735
|
+
return;
|
|
736
|
+
this.overrideWarningCatalogs.set(partition, catalog);
|
|
737
|
+
}
|
|
843
738
|
const known = new Set(tools.map((tool) => tool.name));
|
|
844
739
|
for (const name of Object.keys(overrides ?? {})) {
|
|
845
|
-
if (
|
|
740
|
+
if (known.has(name))
|
|
741
|
+
continue;
|
|
742
|
+
if (this.connectors.get(id)?.staticTools) {
|
|
846
743
|
throw new Error(`ConnectaConfig.classification: connector "${id}" has no tool "${name}"`);
|
|
744
|
+
}
|
|
745
|
+
// Only deployment-authored names, never downstream text. JSON quoting
|
|
746
|
+
// prevents control characters from forging operator log lines.
|
|
747
|
+
const quoted = JSON.stringify(`${id}.${name}`).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
748
|
+
this.opts.logger.warn(`classification override ${quoted} is dormant because the runtime catalog lists no such tool`);
|
|
847
749
|
}
|
|
848
750
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (!connector)
|
|
852
|
-
throw new Error(`Unknown connector "${id}"`);
|
|
853
|
-
if (!requestScope) {
|
|
854
|
-
return this.loadTools(id, baseUrl, requestScope, callOptions, scope);
|
|
855
|
-
}
|
|
856
|
-
let loads = this.requestCatalogLoads.get(requestScope);
|
|
857
|
-
if (!loads) {
|
|
858
|
-
loads = new Map();
|
|
859
|
-
this.requestCatalogLoads.set(requestScope, loads);
|
|
860
|
-
}
|
|
861
|
-
const requestLoads = loads;
|
|
862
|
-
const key = JSON.stringify([id, scope?.principalKey, scope?.subjectKey, scope?.caller]);
|
|
863
|
-
let load = requestLoads.get(key);
|
|
864
|
-
if (!load) {
|
|
865
|
-
const started = new SharedRead((signal) => this.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, scope),
|
|
866
|
-
// Settled or cancelled, the read leaves the map before any caller
|
|
867
|
-
// resumes, so the next one asks the caches afresh.
|
|
868
|
-
() => {
|
|
869
|
-
if (requestLoads.get(key) === started)
|
|
870
|
-
requestLoads.delete(key);
|
|
871
|
-
if (requestLoads.size === 0) {
|
|
872
|
-
this.requestCatalogLoads.delete(requestScope);
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
requestLoads.set(key, started);
|
|
876
|
-
load = started;
|
|
877
|
-
}
|
|
878
|
-
return runEdge(load.join(callOptions.signal));
|
|
751
|
+
getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
752
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "getTools", id, baseUrl, requestScope, callOptions, scope));
|
|
879
753
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const vault = this.opts.credentialVault;
|
|
883
|
-
if (!credential || !vault)
|
|
884
|
-
return undefined;
|
|
885
|
-
try {
|
|
886
|
-
const values = await vault.getAll(id, this.opts.credentialOwner);
|
|
887
|
-
const shape = storedCredentialShape(credential, values);
|
|
888
|
-
return shape.state === "mismatch" ? shape.message : undefined;
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
logFailure(this.opts.logger, "credential shape read failed", failureRecord({ connector: id }, error));
|
|
892
|
-
return undefined;
|
|
893
|
-
}
|
|
754
|
+
credentialDriftFor(id) {
|
|
755
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id));
|
|
894
756
|
}
|
|
895
757
|
/** Age of a complete cache entry; reads never refresh it. */
|
|
896
758
|
catalogAgeMs(id) {
|
|
@@ -898,76 +760,225 @@ export class Registry {
|
|
|
898
760
|
return observedAt === undefined ? null : Math.max(0, Date.now() - observedAt);
|
|
899
761
|
}
|
|
900
762
|
/** Pure lookup shared by status probes and operator catalog refreshes. */
|
|
901
|
-
withCatalogObservations(id,
|
|
763
|
+
withCatalogObservations(id, _baseUrl, status, _scope) {
|
|
902
764
|
const connector = this.connectors.get(id);
|
|
903
765
|
if (!connector)
|
|
904
766
|
return ownStatus({ state: "error", message: "Unknown connector" });
|
|
905
|
-
const report = this.catalogDriftOf(connector, baseUrl, scope);
|
|
906
767
|
const access = this.catalogAccess.get(id);
|
|
907
768
|
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
908
769
|
// it cannot smuggle payload or another partition's observations (INV-6).
|
|
909
|
-
return Object.assign(boundedStatus(status), {
|
|
910
|
-
...(report ? { catalogDrift: report } : {}),
|
|
911
|
-
...(access ? { catalogAccess: { ...access } } : {}),
|
|
912
|
-
});
|
|
770
|
+
return Object.assign(boundedStatus(status), access ? { catalogAccess: { ...access } } : {});
|
|
913
771
|
}
|
|
914
772
|
/** Best-effort connector status for the operator UI. */
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (!connector)
|
|
918
|
-
return ownStatus({ state: "error", message: "Unknown connector" });
|
|
919
|
-
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
920
|
-
// Whatever the state turns out to be, it carries the drift the last
|
|
921
|
-
// refresh saw. Reading it is a lookup, not a probe: a connector that has
|
|
922
|
-
// listed nothing yet reports nothing, and status never lists on its own to
|
|
923
|
-
// make the field appear.
|
|
924
|
-
// The report is rebuilt rather than spread through: what the seam returned
|
|
925
|
-
// is third-party output, and status is read by the operator UI and copied
|
|
926
|
-
// into responses.
|
|
927
|
-
const withObservations = (status) => this.withCatalogObservations(id, baseUrl, status, scope);
|
|
928
|
-
// Credential declarations own their readiness even when a static catalog
|
|
929
|
-
// or a plugin status would otherwise report success. Never probe an empty slot.
|
|
930
|
-
if (connector.credential) {
|
|
931
|
-
try {
|
|
932
|
-
const values = (await this.opts.credentialVault?.getAll(id, this.opts.credentialOwner)) ?? null;
|
|
933
|
-
const shape = storedCredentialShape(connector.credential, values);
|
|
934
|
-
if (shape.state === "missing")
|
|
935
|
-
return withObservations(ownStatus({ state: "credential_required" }));
|
|
936
|
-
if (shape.state === "mismatch")
|
|
937
|
-
return withObservations(ownStatus({ state: "auth_required" }));
|
|
938
|
-
}
|
|
939
|
-
catch (err) {
|
|
940
|
-
return withObservations(failureStatus(id, err));
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
if (connector.status) {
|
|
944
|
-
try {
|
|
945
|
-
return withObservations(await connector.status(ctx));
|
|
946
|
-
}
|
|
947
|
-
catch (err) {
|
|
948
|
-
return withObservations(failureStatus(id, err));
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
try {
|
|
952
|
-
await this.getTools(id, baseUrl, requestScope, callOptions, scope);
|
|
953
|
-
return withObservations({ state: "ok" });
|
|
954
|
-
}
|
|
955
|
-
catch (err) {
|
|
956
|
-
return withObservations(failureStatus(id, err));
|
|
957
|
-
}
|
|
773
|
+
statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
774
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "statusFor", id, baseUrl, requestScope, callOptions, scope));
|
|
958
775
|
}
|
|
959
776
|
/** Rotate the storage generation; in-flight old readers cannot republish it. */
|
|
960
777
|
invalidate(id) {
|
|
961
778
|
void this.invalidateStored(id);
|
|
962
779
|
}
|
|
963
|
-
|
|
964
|
-
this
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
780
|
+
invalidateStored(id) {
|
|
781
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "invalidateStored", id));
|
|
782
|
+
}
|
|
783
|
+
static {
|
|
784
|
+
provideRegistryPrograms(Registry.prototype, {
|
|
785
|
+
stashResult: (self, id, chunks, ttlSeconds, partition = scopes.results) => Effect.gen({ self }, function* () {
|
|
786
|
+
const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
|
|
787
|
+
const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
|
|
788
|
+
// The paging envelope is ASCII, so its string length is its stored byte count.
|
|
789
|
+
const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
790
|
+
if (bytes > maxBytes || maxEntries === 0)
|
|
791
|
+
return false;
|
|
792
|
+
const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
|
|
793
|
+
// IDs belong to reservations, not reusable result IDs or subjects.
|
|
794
|
+
const charge = crypto.randomUUID();
|
|
795
|
+
const ttlMs = ttlSeconds * 1000;
|
|
796
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
797
|
+
return false;
|
|
798
|
+
const booking = yield* swapStashLedger((live, now) => {
|
|
799
|
+
// A committed CAS with a lost response is already our booking, even
|
|
800
|
+
// at full capacity. Reuse its deadline instead of adding a second slot.
|
|
801
|
+
const own = live.find((entry) => entry[0] === charge);
|
|
802
|
+
if (own)
|
|
803
|
+
return { result: own[2] };
|
|
804
|
+
const used = live.reduce((sum, entry) => sum + entry[1], 0);
|
|
805
|
+
if (live.length >= maxEntries || used + bytes > maxBytes)
|
|
806
|
+
return { result: undefined };
|
|
807
|
+
const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
|
|
808
|
+
if (!Number.isSafeInteger(deadline))
|
|
809
|
+
return { result: undefined };
|
|
810
|
+
return { entries: [...live, [charge, bytes, deadline]], result: deadline };
|
|
811
|
+
});
|
|
812
|
+
if (booking.status === "exhausted" || booking.result === undefined)
|
|
813
|
+
return false;
|
|
814
|
+
const deadline = booking.result;
|
|
815
|
+
const writeDeadline = deadline - ttlMs;
|
|
816
|
+
const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
|
|
817
|
+
const owns = (entry) => entry[0] === charge;
|
|
818
|
+
const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
|
|
819
|
+
? {
|
|
820
|
+
entries: live.map((entry) => owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry),
|
|
821
|
+
result: undefined,
|
|
822
|
+
}
|
|
823
|
+
: { result: undefined });
|
|
824
|
+
let pendingWrite = false;
|
|
825
|
+
// A rejected or timed-out driver call may still commit after deletion.
|
|
826
|
+
// Only a confirmed write response clears pendingWrite.
|
|
827
|
+
const release = completeStash(Effect.gen(function* () {
|
|
828
|
+
const deleted = yield* Effect.gen(function* () {
|
|
829
|
+
for (const key of keys)
|
|
830
|
+
yield* storageDelete(key);
|
|
831
|
+
return true;
|
|
832
|
+
}).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
833
|
+
if (pendingWrite)
|
|
834
|
+
return;
|
|
835
|
+
if (deleted) {
|
|
836
|
+
const released = yield* swapStashLedger((live) => live.some(owns)
|
|
837
|
+
? { entries: live.filter((entry) => !owns(entry)), result: undefined }
|
|
838
|
+
: { result: undefined });
|
|
839
|
+
if (released.status === "applied")
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
yield* settle;
|
|
843
|
+
}));
|
|
844
|
+
// Trailing chunks first, then the header. The caller receives no result
|
|
845
|
+
// ID after failure or timeout, including a late header commit.
|
|
846
|
+
const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
|
|
847
|
+
const written = yield* Effect.gen(function* () {
|
|
848
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
849
|
+
const before = yield* Clock.currentTimeMillis;
|
|
850
|
+
if (before >= writeDeadline || before >= chunkExpiry)
|
|
851
|
+
return false;
|
|
852
|
+
pendingWrite = true;
|
|
853
|
+
yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
854
|
+
pendingWrite = false;
|
|
855
|
+
})));
|
|
856
|
+
if ((yield* Clock.currentTimeMillis) >= writeDeadline)
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
return true;
|
|
860
|
+
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
|
|
861
|
+
if (!written)
|
|
862
|
+
yield* release;
|
|
863
|
+
else
|
|
864
|
+
yield* completeStash(settle);
|
|
865
|
+
return written;
|
|
866
|
+
}).pipe(Effect.provideService(Storage, self.opts.storage)),
|
|
867
|
+
recheckResultAccess: () => Effect.succeed(true),
|
|
868
|
+
storeOAuthHandoff: (self, connectorId, state, principalKey) => Effect.gen(function* () {
|
|
869
|
+
const key = oauthHandoffKeys.handoff(connectorId, yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error }));
|
|
870
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
871
|
+
const existing = yield* storageGet(key);
|
|
872
|
+
if (existing && existing !== principalKey)
|
|
873
|
+
throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
|
|
874
|
+
if (yield* storageCompareAndSet(key, existing, principalKey, { ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS }))
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
throw new Error(`OAuth handoff for "${connectorId}" is busy; retry authorization`);
|
|
878
|
+
}).pipe(Effect.provideService(Storage, self.opts.storage)),
|
|
879
|
+
oauthCallbackView: (self, connectorId, state) => Effect.gen(function* () {
|
|
880
|
+
if (!state)
|
|
881
|
+
return null;
|
|
882
|
+
const digest = yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error });
|
|
883
|
+
const principalKey = yield* storageGet(oauthHandoffKeys.handoff(connectorId, digest));
|
|
884
|
+
const connector = self.connectors.get(connectorId);
|
|
885
|
+
if (!connector || !principalKey)
|
|
886
|
+
return null;
|
|
887
|
+
return {
|
|
888
|
+
registry: connector.authScope === "personal"
|
|
889
|
+
? self.scoped({ connectorIds: [connectorId], subjectKey: principalKey, principalKey })
|
|
890
|
+
: self,
|
|
891
|
+
principalKey,
|
|
892
|
+
};
|
|
893
|
+
}).pipe(Effect.provideService(Storage, self.opts.storage)),
|
|
894
|
+
consumeOAuthHandoff: (self, connectorId, state, principalKey) => Effect.gen(function* () {
|
|
895
|
+
if (!state)
|
|
896
|
+
return false;
|
|
897
|
+
const digest = yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error });
|
|
898
|
+
return yield* storageCompareAndSet(oauthHandoffKeys.handoff(connectorId, digest), principalKey, null);
|
|
899
|
+
}).pipe(Effect.provideService(Storage, self.opts.storage)),
|
|
900
|
+
bindOAuthHandoff: (self, id, authorizationUrl, principalKey) => Effect.gen(function* () {
|
|
901
|
+
const state = new URL(authorizationUrl).searchParams.get("state");
|
|
902
|
+
if (!state || !principalKey)
|
|
903
|
+
throw new Error("OAuth requires state and an initiating user");
|
|
904
|
+
yield* registryProgram(self, "storeOAuthHandoff", id, state, principalKey);
|
|
905
|
+
}),
|
|
906
|
+
getTools: (self, id, baseUrl, requestScope, callOptions = {}, scope) => Effect.suspend(() => {
|
|
907
|
+
if (!self.connectors.has(id))
|
|
908
|
+
return Effect.fail(new Error(`Unknown connector "${id}"`));
|
|
909
|
+
const owner = self.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
910
|
+
return duringViewEffect(owner, (signal) => {
|
|
911
|
+
if (!requestScope)
|
|
912
|
+
return self.loadTools(id, baseUrl, owner.requestScope, { ...callOptions, signal }, scope);
|
|
913
|
+
let loads = self.requestCatalogLoads.get(requestScope);
|
|
914
|
+
if (!loads) {
|
|
915
|
+
loads = new Map();
|
|
916
|
+
self.requestCatalogLoads.set(requestScope, loads);
|
|
917
|
+
}
|
|
918
|
+
const requestLoads = loads;
|
|
919
|
+
const key = JSON.stringify([id, scope?.principalKey, scope?.subjectKey, scope?.caller]);
|
|
920
|
+
let load = requestLoads.get(key);
|
|
921
|
+
if (!load) {
|
|
922
|
+
const started = new SharedRead((signal) => self.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, scope), () => {
|
|
923
|
+
if (requestLoads.get(key) === started)
|
|
924
|
+
requestLoads.delete(key);
|
|
925
|
+
if (requestLoads.size === 0)
|
|
926
|
+
self.requestCatalogLoads.delete(requestScope);
|
|
927
|
+
}, requestScope);
|
|
928
|
+
requestLoads.set(key, started);
|
|
929
|
+
load = started;
|
|
930
|
+
}
|
|
931
|
+
return load.join(signal);
|
|
932
|
+
});
|
|
933
|
+
}),
|
|
934
|
+
credentialDriftFor: (self, id) => Effect.gen(function* () {
|
|
935
|
+
const credential = self.connectors.get(id)?.credential;
|
|
936
|
+
const vault = self.opts.credentialVault;
|
|
937
|
+
if (!credential || !vault)
|
|
938
|
+
return undefined;
|
|
939
|
+
return yield* Effect.tryPromise({
|
|
940
|
+
try: () => vault.getAll(id, self.opts.credentialOwner),
|
|
941
|
+
catch: (error) => error,
|
|
942
|
+
}).pipe(Effect.map((values) => {
|
|
943
|
+
const shape = storedCredentialShape(credential, values);
|
|
944
|
+
return shape.state === "mismatch" ? shape.message : undefined;
|
|
945
|
+
}), Effect.catchCause((cause) => Effect.sync(() => {
|
|
946
|
+
logFailure(self.opts.logger, "credential shape read failed", failureRecord({ connector: id }, causeError(cause)));
|
|
947
|
+
return undefined;
|
|
948
|
+
})));
|
|
949
|
+
}),
|
|
950
|
+
statusFor: (self, id, baseUrl, requestScope = {}, callOptions = {}, scope) => Effect.suspend(() => {
|
|
951
|
+
const connector = self.connectors.get(id);
|
|
952
|
+
if (!connector)
|
|
953
|
+
return Effect.succeed(ownStatus({ state: "error", message: "Unknown connector" }));
|
|
954
|
+
const ctx = self.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
955
|
+
const withObservations = (status) => self.withCatalogObservations(id, baseUrl, status, scope);
|
|
956
|
+
return duringViewEffect(ctx, () => Effect.gen(function* () {
|
|
957
|
+
if (connector.credential) {
|
|
958
|
+
const values = yield* Effect.tryPromise({
|
|
959
|
+
try: () => self.opts.credentialVault?.getAll(id, self.opts.credentialOwner) ?? Promise.resolve(null),
|
|
960
|
+
catch: (error) => error,
|
|
961
|
+
});
|
|
962
|
+
const shape = storedCredentialShape(connector.credential, values);
|
|
963
|
+
if (shape.state === "missing")
|
|
964
|
+
return withObservations(ownStatus({ state: "credential_required" }));
|
|
965
|
+
if (shape.state === "mismatch")
|
|
966
|
+
return withObservations(ownStatus({ state: "auth_required" }));
|
|
967
|
+
}
|
|
968
|
+
if (connector.status)
|
|
969
|
+
return withObservations(yield* authInvoke(connector, "status", [ctx]));
|
|
970
|
+
yield* registryProgram(self, "getTools", id, baseUrl, requestScope, callOptions, scope);
|
|
971
|
+
return withObservations({ state: "ok" });
|
|
972
|
+
})).pipe(Effect.catchCause((cause) => Effect.succeed(withObservations(failureStatus(id, causeError(cause, ctx.signal))))));
|
|
973
|
+
}),
|
|
974
|
+
invalidateStored: (self, id) => Effect.suspend(() => {
|
|
975
|
+
self.catalogObservedAt.delete(id);
|
|
976
|
+
return Effect.tryPromise({
|
|
977
|
+
try: () => invalidateCatalogCache(self.opts.catalogStorage ?? self.opts.storage, id),
|
|
978
|
+
catch: (error) => error,
|
|
979
|
+
}).pipe(Effect.catch((error) => Effect.sync(() => logFailure(self.opts.logger, "catalog invalidation failed", failureRecord({ connector: id }, error)))));
|
|
980
|
+
}),
|
|
981
|
+
});
|
|
971
982
|
}
|
|
972
983
|
}
|
|
973
984
|
class ScopedRegistryView {
|
|
@@ -1033,27 +1044,8 @@ class ScopedRegistryView {
|
|
|
1033
1044
|
const connector = this.getConnector(parsed.connectorId);
|
|
1034
1045
|
return connector ? { connector, toolName: parsed.toolName } : null;
|
|
1035
1046
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (!registry) {
|
|
1039
|
-
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1040
|
-
}
|
|
1041
|
-
const tools = await registry.getTools(args[0], args[1], args[2], args[3], this.scope);
|
|
1042
|
-
const granted = this.scope.toolAccess?.get(args[0]);
|
|
1043
|
-
if (!granted)
|
|
1044
|
-
return tools;
|
|
1045
|
-
// Every consumer — search, describe, call_tool, and a program's
|
|
1046
|
-
// connecta.call — resolves through this list, so an ungranted tool is
|
|
1047
|
-
// indistinguishable from one the connector never had.
|
|
1048
|
-
const visible = tools.filter((tool) => granted.has(tool.name));
|
|
1049
|
-
if (visible.length < granted.size) {
|
|
1050
|
-
const present = new Set(tools.map((tool) => tool.name));
|
|
1051
|
-
for (const name of granted) {
|
|
1052
|
-
if (!present.has(name))
|
|
1053
|
-
this.root.noteAbsentGrant(args[0], name);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
return carryCatalogFreshness(tools, visible);
|
|
1047
|
+
getTools(...args) {
|
|
1048
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "getTools", ...args));
|
|
1057
1049
|
}
|
|
1058
1050
|
contextFor(...args) {
|
|
1059
1051
|
const registry = this.registryFor(args[0]);
|
|
@@ -1079,15 +1071,14 @@ class ScopedRegistryView {
|
|
|
1079
1071
|
origin: this.scope.origin ?? null,
|
|
1080
1072
|
};
|
|
1081
1073
|
}
|
|
1082
|
-
recheckResultAccess(
|
|
1083
|
-
return this.scope.currentResultAccess?.(
|
|
1074
|
+
recheckResultAccess(dependencies, classification, signal) {
|
|
1075
|
+
return this.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true);
|
|
1084
1076
|
}
|
|
1085
1077
|
resultsStorage() {
|
|
1086
1078
|
return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
|
|
1087
1079
|
}
|
|
1088
1080
|
credentialDriftFor(id) {
|
|
1089
|
-
|
|
1090
|
-
return registry ? registry.credentialDriftFor(id) : Promise.resolve(undefined);
|
|
1081
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id));
|
|
1091
1082
|
}
|
|
1092
1083
|
observedOutputSchema(connectorId, definition) {
|
|
1093
1084
|
return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
|
|
@@ -1099,23 +1090,65 @@ class ScopedRegistryView {
|
|
|
1099
1090
|
return this.registryFor(id)?.catalogAgeMs(id) ?? null;
|
|
1100
1091
|
}
|
|
1101
1092
|
statusFor(...args) {
|
|
1102
|
-
|
|
1103
|
-
return registry
|
|
1104
|
-
? registry.statusFor(args[0], args[1], args[2], args[3], this.scope)
|
|
1105
|
-
: Promise.resolve({ state: "error", message: "Unknown connector" });
|
|
1093
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "statusFor", ...args));
|
|
1106
1094
|
}
|
|
1107
1095
|
withCatalogObservations(id, baseUrl, status) {
|
|
1108
1096
|
return (this.registryFor(id)?.withCatalogObservations(id, baseUrl, status, this.scope) ??
|
|
1109
1097
|
ownStatus({ state: "error", message: "Unknown connector" }));
|
|
1110
1098
|
}
|
|
1111
1099
|
invalidateStored(id) {
|
|
1112
|
-
|
|
1113
|
-
return registry ? registry.invalidateStored(id) : Promise.resolve();
|
|
1100
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "invalidateStored", id));
|
|
1114
1101
|
}
|
|
1115
1102
|
credentialUiAvailable() {
|
|
1116
1103
|
return this.root.credentialUiAvailable();
|
|
1117
1104
|
}
|
|
1118
1105
|
async bindOAuthHandoff(id, authorizationUrl, principalKey) {
|
|
1119
|
-
|
|
1106
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "bindOAuthHandoff", id, authorizationUrl, principalKey));
|
|
1107
|
+
}
|
|
1108
|
+
static {
|
|
1109
|
+
provideRegistryPrograms(ScopedRegistryView.prototype, {
|
|
1110
|
+
stashResult: (self, id, chunks, ttlSeconds) => registryProgram(self.root, "stashResult", id, chunks, ttlSeconds, self.scope.subjectKey ? scopes.subject(self.scope.subjectKey) : scopes.results),
|
|
1111
|
+
recheckResultAccess: (self, dependencies, classification, signal) => Effect.tryPromise({
|
|
1112
|
+
try: () => self.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true),
|
|
1113
|
+
catch: (error) => error,
|
|
1114
|
+
}),
|
|
1115
|
+
getTools: (self, ...args) => Effect.gen(function* () {
|
|
1116
|
+
const registry = self.registryFor(args[0]);
|
|
1117
|
+
if (!registry) {
|
|
1118
|
+
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1119
|
+
}
|
|
1120
|
+
const tools = yield* registryProgram(registry, "getTools", args[0], args[1], args[2], args[3], self.scope);
|
|
1121
|
+
const granted = self.scope.toolAccess?.get(args[0]);
|
|
1122
|
+
if (!granted)
|
|
1123
|
+
return tools;
|
|
1124
|
+
// Every consumer — search, describe, call_tool, and a program's
|
|
1125
|
+
// connecta.call — resolves through this list, so an ungranted tool is
|
|
1126
|
+
// indistinguishable from one the connector never had.
|
|
1127
|
+
const visible = tools.filter((tool) => granted.has(tool.name));
|
|
1128
|
+
if (visible.length < granted.size) {
|
|
1129
|
+
const present = new Set(tools.map((tool) => tool.name));
|
|
1130
|
+
for (const name of granted) {
|
|
1131
|
+
if (!present.has(name))
|
|
1132
|
+
self.root.noteAbsentGrant(args[0], name);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return carryCatalogFreshness(tools, visible);
|
|
1136
|
+
}),
|
|
1137
|
+
credentialDriftFor: (self, id) => Effect.suspend(() => {
|
|
1138
|
+
const registry = self.registryFor(id);
|
|
1139
|
+
return registry ? registryProgram(registry, "credentialDriftFor", id) : Effect.succeed(undefined);
|
|
1140
|
+
}),
|
|
1141
|
+
statusFor: (self, ...args) => Effect.suspend(() => {
|
|
1142
|
+
const registry = self.registryFor(args[0]);
|
|
1143
|
+
return registry
|
|
1144
|
+
? registryProgram(registry, "statusFor", args[0], args[1], args[2], args[3], self.scope)
|
|
1145
|
+
: Effect.succeed({ state: "error", message: "Unknown connector" });
|
|
1146
|
+
}),
|
|
1147
|
+
invalidateStored: (self, id) => Effect.suspend(() => {
|
|
1148
|
+
const registry = self.registryFor(id);
|
|
1149
|
+
return registry ? registryProgram(registry, "invalidateStored", id) : Effect.succeed(undefined);
|
|
1150
|
+
}),
|
|
1151
|
+
bindOAuthHandoff: (self, id, authorizationUrl, principalKey) => registryProgram(self.root, "bindOAuthHandoff", id, authorizationUrl, principalKey ?? self.scope.principalKey),
|
|
1152
|
+
});
|
|
1120
1153
|
}
|
|
1121
1154
|
}
|