@zackbart/connecta 0.34.0 → 0.35.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 +393 -0
- package/README.md +62 -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 +2051 -1160
- 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 +663 -271
- 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 +15 -7
- package/dist/connectors/api-connector.js +147 -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/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +12 -30
- package/dist/connectors/remote-mcp.js +960 -564
- package/dist/d1.js +10 -243
- package/dist/derived-context.js +5 -0
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +3 -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 +228 -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 +383 -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 +221 -87
- 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 +534 -408
- 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/agents.d.ts +5 -4
- package/dist/operator-ui/agents.js +17 -7
- package/dist/operator-ui/config-summary.js +8 -4
- package/dist/operator-ui/connection-map.js +9 -6
- package/dist/operator-ui/contract.d.ts +7 -3
- package/dist/operator-ui/endpoints.js +5 -14
- package/dist/operator-ui/fix-prompts.d.ts +1 -1
- package/dist/operator-ui/fix-prompts.js +0 -8
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/model.d.ts +2 -3
- package/dist/operator-ui/overview.js +11 -19
- package/dist/operator-ui/setup-commands.js +0 -4
- package/dist/operator-ui/view.d.ts +5 -29
- package/dist/operator-ui/view.js +3 -59
- 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 +71 -32
- package/dist/provider.js +134 -40
- package/dist/providers/_shared/google/hatch.js +91 -0
- package/dist/providers/_shared/google/kit.js +537 -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 +8 -209
- package/dist/providers/_shared/google/workspace.js +7 -426
- package/dist/providers/_shared/rest/compact-index.js +86 -0
- package/dist/providers/_shared/rest/dispatch.js +3 -23
- package/dist/providers/_shared/rest/kit.js +546 -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/tools.js +101 -78
- 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 +3 -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 +17 -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 +7 -22
- package/dist/providers/cloudflare/index.js +80 -194
- 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 +3299 -3208
- package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
- 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 +802 -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 +1024 -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 +3 -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 +6 -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/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 +734 -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 +7 -27
- package/dist/providers/stripe/index.js +75 -191
- 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 -10
- package/dist/providers/stripe/tools.js +77 -0
- package/dist/providers/tithely/api.js +531 -0
- package/dist/providers/tithely/index.d.ts +19 -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 +11 -25
- package/dist/providers/vercel/index.js +67 -261
- 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 +398 -376
- package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
- 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 +30 -28
- package/dist/registry.js +446 -413
- 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 +3 -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 +29 -22
- 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 +556 -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 +330 -224
- 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 +11 -99
- 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 +27 -7
- package/documentation/architecture.md +2550 -186
- package/documentation/auth.md +821 -98
- package/documentation/code-mode.md +56 -21
- package/documentation/deploying.md +164 -20
- package/documentation/hosted-mcp-migration.md +460 -0
- package/documentation/integrating.md +144 -105
- package/documentation/meta-tools.md +411 -34
- package/documentation/operating.md +28 -4
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +103 -33
- package/documentation/provider-auth-migration.md +38 -63
- package/documentation/provider-migration-0.29.md +10 -5
- package/documentation/provider-migration-0.35.md +975 -0
- package/documentation/providers.md +965 -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 +27 -42
- 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 -134
- package/dist/catalog-drift.js +0 -493
- package/dist/providers/_shared/request-token.js +0 -83
- 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/value-safety.d.ts +0 -201
- package/dist/providers/basecamp/index.d.ts +0 -43
- 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 -1078
- 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/linear/index.d.ts +0 -55
- package/dist/providers/linear/index.js +0 -352
- package/dist/providers/linear/skill.generated.js +0 -8
- package/dist/providers/mixpanel/index.d.ts +0 -57
- package/dist/providers/mixpanel/index.js +0 -468
- 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 -65
- 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 -49
- package/dist/providers/revenuecat/index.js +0 -557
- 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;
|
|
@@ -229,13 +246,9 @@ export class Registry {
|
|
|
229
246
|
catalogObservedAt = new Map();
|
|
230
247
|
/** Last payload-free agent catalog access in this runtime. */
|
|
231
248
|
catalogAccess = new Map();
|
|
232
|
-
/**
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* Schema digest verification per reviewed facts array. Only the registry's
|
|
236
|
-
* own deep-frozen arrays are keys, so an entry stays true while they live.
|
|
237
|
-
*/
|
|
238
|
-
verifiedFacts = new WeakMap();
|
|
249
|
+
/** Bounded tokens isolating count-only observations by catalog identity. */
|
|
250
|
+
catalogObservationPartitions = new Map();
|
|
251
|
+
overrideWarningCatalogs = new WeakMap();
|
|
239
252
|
observedOutputSchemas;
|
|
240
253
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
241
254
|
maxResultBytes;
|
|
@@ -263,6 +276,9 @@ export class Registry {
|
|
|
263
276
|
if (this.connectors.has(c.id)) {
|
|
264
277
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
265
278
|
}
|
|
279
|
+
if (c.readOnly !== undefined && typeof c.readOnly !== "boolean") {
|
|
280
|
+
throw new Error(`Connector "${c.id}" readOnly must be a boolean.`);
|
|
281
|
+
}
|
|
266
282
|
if (c.authScope !== undefined && c.authScope !== "shared" && c.authScope !== "personal") {
|
|
267
283
|
throw new Error(`Invalid authScope on connector "${c.id}": expected "shared" or "personal"`);
|
|
268
284
|
}
|
|
@@ -273,11 +289,6 @@ export class Registry {
|
|
|
273
289
|
`normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
|
|
274
290
|
"Shorten it or omit it to derive one.");
|
|
275
291
|
}
|
|
276
|
-
// Validated here, before any request (INV-11), and read once per
|
|
277
|
-
// connector object.
|
|
278
|
-
if (catalogReviewOf(c) && c.staticTools) {
|
|
279
|
-
throw new Error(`Connector "${c.id}" declares both staticTools and a classification; ` + "annotate static tools directly.");
|
|
280
|
-
}
|
|
281
292
|
this.connectors.set(c.id, c);
|
|
282
293
|
if (c.callAdmission !== undefined) {
|
|
283
294
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
@@ -290,8 +301,8 @@ export class Registry {
|
|
|
290
301
|
Object.freeze(this.classification);
|
|
291
302
|
for (const [id, overrides] of Object.entries(this.classification)) {
|
|
292
303
|
for (const [name, verdict] of Object.entries(overrides)) {
|
|
293
|
-
if (!name || (verdict !== "read" && verdict !== "write")) {
|
|
294
|
-
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"`);
|
|
295
306
|
}
|
|
296
307
|
}
|
|
297
308
|
const connector = this.connectors.get(id);
|
|
@@ -328,13 +339,11 @@ export class Registry {
|
|
|
328
339
|
...this.opts,
|
|
329
340
|
classification: Object.fromEntries(Object.entries(this.classification).filter(([id]) => this.connectors.get(id)?.authScope === "personal")),
|
|
330
341
|
storage: namespaced(this.opts.storage, scopes.principal(principalKey)),
|
|
342
|
+
rootStorage: this.opts.rootStorage ?? this.opts.storage,
|
|
331
343
|
credentialOwner: principalKey,
|
|
332
344
|
catalogStorage: this.opts.catalogStorage ?? this.opts.storage,
|
|
333
345
|
constructionChecks: false,
|
|
334
346
|
});
|
|
335
|
-
// Counts only, like the connector-wide reviewed drift observation. No
|
|
336
|
-
// principal, credentials, or catalog payload crosses this registry boundary.
|
|
337
|
-
registry.droppedToolNames = this.droppedToolNames;
|
|
338
347
|
if (this.callAdmissionClosed)
|
|
339
348
|
registry.closeCallAdmission();
|
|
340
349
|
this.personalRegistries.set(principalKey, registry);
|
|
@@ -378,45 +387,14 @@ export class Registry {
|
|
|
378
387
|
const quoted = JSON.stringify(key).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
379
388
|
this.opts.logger.warn(`a pool grants ${quoted} but connector "${connectorId}" lists no such tool; the grant is unreachable`);
|
|
380
389
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
for (let attempt = 0; attempt < 32; attempt++) {
|
|
384
|
-
const existing = await this.opts.storage.get(key);
|
|
385
|
-
if (existing && existing !== principalKey) {
|
|
386
|
-
throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
|
|
387
|
-
}
|
|
388
|
-
// Bind ownership atomically, including renewal of the same owner's
|
|
389
|
-
// handoff. Two owners reading a miss must not overwrite each other.
|
|
390
|
-
if (await this.opts.storage.compareAndSet(key, existing, principalKey, {
|
|
391
|
-
ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS,
|
|
392
|
-
}))
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
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));
|
|
396
392
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return null;
|
|
400
|
-
const principalKey = await this.opts.storage.get(oauthHandoffKeys.handoff(connectorId, await sha256Hex(state)));
|
|
401
|
-
const connector = this.connectors.get(connectorId);
|
|
402
|
-
if (!connector || !principalKey)
|
|
403
|
-
return null;
|
|
404
|
-
return {
|
|
405
|
-
registry: connector.authScope === "personal"
|
|
406
|
-
? this.scoped({
|
|
407
|
-
connectorIds: [connectorId],
|
|
408
|
-
subjectKey: principalKey,
|
|
409
|
-
principalKey,
|
|
410
|
-
})
|
|
411
|
-
: this,
|
|
412
|
-
principalKey,
|
|
413
|
-
};
|
|
393
|
+
oauthCallbackView(connectorId, state) {
|
|
394
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "oauthCallbackView", connectorId, state));
|
|
414
395
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return false;
|
|
418
|
-
const key = oauthHandoffKeys.handoff(connectorId, await sha256Hex(state));
|
|
419
|
-
return this.opts.storage.compareAndSet(key, principalKey, null);
|
|
396
|
+
consumeOAuthHandoff(connectorId, state, principalKey) {
|
|
397
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "consumeOAuthHandoff", connectorId, state, principalKey));
|
|
420
398
|
}
|
|
421
399
|
/**
|
|
422
400
|
* Refuse an unusable result cap at construction, like every other
|
|
@@ -470,18 +448,22 @@ export class Registry {
|
|
|
470
448
|
return this.connectors.has(id);
|
|
471
449
|
}
|
|
472
450
|
contextFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
473
|
-
const
|
|
451
|
+
const connector = this.connectors.get(id);
|
|
452
|
+
const credentialConfig = connector?.credential;
|
|
474
453
|
let credentialAccess;
|
|
454
|
+
let credentialProgram;
|
|
475
455
|
if (this.opts.credentialVault && credentialConfig) {
|
|
476
456
|
const vault = this.opts.credentialVault;
|
|
477
|
-
|
|
478
|
-
|
|
457
|
+
credentialProgram = Effect.tryPromise({
|
|
458
|
+
try: () => vault.getAll(id, this.opts.credentialOwner),
|
|
459
|
+
catch: (error) => error,
|
|
460
|
+
}).pipe(Effect.flatMap((values) => {
|
|
479
461
|
const shape = storedCredentialShape(credentialConfig, values);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
462
|
+
return shape.state === "mismatch"
|
|
463
|
+
? Effect.fail(new ConnectorCallError("auth_required", shape.message))
|
|
464
|
+
: Effect.succeed(values);
|
|
465
|
+
}));
|
|
466
|
+
const readValues = () => runEdge(credentialProgram);
|
|
485
467
|
credentialAccess = {
|
|
486
468
|
get: async (field = "value") => {
|
|
487
469
|
const values = await readValues();
|
|
@@ -490,7 +472,7 @@ export class Registry {
|
|
|
490
472
|
getAll: readValues,
|
|
491
473
|
};
|
|
492
474
|
}
|
|
493
|
-
const context = {
|
|
475
|
+
const context = operationView({
|
|
494
476
|
storage: namespaced(this.opts.storage, scopes.connector(id)),
|
|
495
477
|
logger: this.opts.logger,
|
|
496
478
|
baseUrl,
|
|
@@ -499,17 +481,19 @@ export class Registry {
|
|
|
499
481
|
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
500
482
|
requestScope,
|
|
501
483
|
...callOptions,
|
|
502
|
-
};
|
|
503
|
-
|
|
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
|
+
});
|
|
504
494
|
attachCatalogCache(context, {
|
|
505
495
|
storage: this.opts.catalogStorage ?? this.opts.storage,
|
|
506
|
-
partition:
|
|
507
|
-
scope?.principalKey ?? this.opts.credentialOwner ?? null,
|
|
508
|
-
scope?.subjectKey ?? null,
|
|
509
|
-
scope?.caller?.identity ?? null,
|
|
510
|
-
scope?.caller?.authenticated ?? false,
|
|
511
|
-
scope?.caller?.pool ?? null,
|
|
512
|
-
]),
|
|
496
|
+
partition: this.catalogPartition(scope),
|
|
513
497
|
sharedPartition: JSON.stringify([baseUrl, this.opts.publicUrl ?? null, scope?.caller?.pool ?? null]),
|
|
514
498
|
defaultTtlMs: (this.opts.toolCacheTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds) * 1000,
|
|
515
499
|
minTtlMs: (this.opts.catalogMinTtlSeconds ?? CONFIG_DEFAULTS.discovery.catalogMinTtlSeconds) * 1000,
|
|
@@ -553,26 +537,27 @@ export class Registry {
|
|
|
553
537
|
}
|
|
554
538
|
return Object.fromEntries([...snapshots].map(([id, values]) => [id, aggregateCallAdmissionSnapshots(values)]));
|
|
555
539
|
}
|
|
556
|
-
/**
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
540
|
+
/** The admitted identity fields used by both catalog caching and reporting. */
|
|
541
|
+
catalogPartition(scope) {
|
|
542
|
+
return JSON.stringify([
|
|
543
|
+
scope?.principalKey ?? this.opts.credentialOwner ?? null,
|
|
544
|
+
scope?.subjectKey ?? null,
|
|
545
|
+
scope?.caller?.identity ?? null,
|
|
546
|
+
scope?.caller?.authenticated ?? false,
|
|
547
|
+
scope?.caller?.pool ?? null,
|
|
548
|
+
]);
|
|
549
|
+
}
|
|
550
|
+
/** Bounded, in-memory tokens; no principal or catalog data is persisted. */
|
|
551
|
+
catalogObservationPartition(id, baseUrl, scope, create = false) {
|
|
552
|
+
const key = JSON.stringify([id, this.catalogPartition(scope), baseUrl, this.opts.publicUrl ?? null]);
|
|
553
|
+
let partition = this.catalogObservationPartitions.get(key);
|
|
554
|
+
if (!partition && create) {
|
|
555
|
+
partition = {};
|
|
556
|
+
this.catalogObservationPartitions.set(key, partition);
|
|
557
|
+
if (this.catalogObservationPartitions.size > 1024)
|
|
558
|
+
this.catalogObservationPartitions.delete(this.catalogObservationPartitions.keys().next().value);
|
|
559
|
+
}
|
|
560
|
+
return partition;
|
|
576
561
|
}
|
|
577
562
|
/** Reject queued/future downstream admission; active permits release safely. */
|
|
578
563
|
closeCallAdmission() {
|
|
@@ -601,88 +586,7 @@ export class Registry {
|
|
|
601
586
|
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
602
587
|
*/
|
|
603
588
|
stashResult(id, chunks, ttlSeconds, partition = scopes.results) {
|
|
604
|
-
return runOnPartition(
|
|
605
|
-
const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
|
|
606
|
-
const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
|
|
607
|
-
// The paging envelope is ASCII, so its string length is its stored byte count.
|
|
608
|
-
const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
609
|
-
if (bytes > maxBytes || maxEntries === 0)
|
|
610
|
-
return false;
|
|
611
|
-
const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
|
|
612
|
-
// IDs belong to reservations, not reusable result IDs or subjects.
|
|
613
|
-
const charge = crypto.randomUUID();
|
|
614
|
-
const ttlMs = ttlSeconds * 1000;
|
|
615
|
-
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
616
|
-
return false;
|
|
617
|
-
const booking = yield* swapStashLedger((live, now) => {
|
|
618
|
-
// A committed CAS with a lost response is already our booking, even
|
|
619
|
-
// at full capacity. Reuse its deadline instead of adding a second slot.
|
|
620
|
-
const own = live.find((entry) => entry[0] === charge);
|
|
621
|
-
if (own)
|
|
622
|
-
return { result: own[2] };
|
|
623
|
-
const used = live.reduce((sum, entry) => sum + entry[1], 0);
|
|
624
|
-
if (live.length >= maxEntries || used + bytes > maxBytes)
|
|
625
|
-
return { result: undefined };
|
|
626
|
-
const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
|
|
627
|
-
if (!Number.isSafeInteger(deadline))
|
|
628
|
-
return { result: undefined };
|
|
629
|
-
return { entries: [...live, [charge, bytes, deadline]], result: deadline };
|
|
630
|
-
});
|
|
631
|
-
if (booking.status === "exhausted" || booking.result === undefined)
|
|
632
|
-
return false;
|
|
633
|
-
const deadline = booking.result;
|
|
634
|
-
const writeDeadline = deadline - ttlMs;
|
|
635
|
-
const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
|
|
636
|
-
const owns = (entry) => entry[0] === charge;
|
|
637
|
-
const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
|
|
638
|
-
? {
|
|
639
|
-
entries: live.map((entry) => (owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry)),
|
|
640
|
-
result: undefined,
|
|
641
|
-
}
|
|
642
|
-
: { result: undefined });
|
|
643
|
-
let pendingWrite = false;
|
|
644
|
-
// A rejected or timed-out driver call may still commit after deletion.
|
|
645
|
-
// Only a confirmed write response clears pendingWrite.
|
|
646
|
-
const release = completeStash(Effect.gen(function* () {
|
|
647
|
-
const deleted = yield* Effect.gen(function* () {
|
|
648
|
-
for (const key of keys)
|
|
649
|
-
yield* storageDelete(key);
|
|
650
|
-
return true;
|
|
651
|
-
}).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
652
|
-
if (pendingWrite)
|
|
653
|
-
return;
|
|
654
|
-
if (deleted) {
|
|
655
|
-
const released = yield* swapStashLedger((live) => live.some(owns)
|
|
656
|
-
? { entries: live.filter((entry) => !owns(entry)), result: undefined }
|
|
657
|
-
: { result: undefined });
|
|
658
|
-
if (released.status === "applied")
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
yield* settle;
|
|
662
|
-
}));
|
|
663
|
-
// Trailing chunks first, then the header. The caller receives no result
|
|
664
|
-
// ID after failure or timeout, including a late header commit.
|
|
665
|
-
const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
|
|
666
|
-
const written = yield* Effect.gen(function* () {
|
|
667
|
-
for (let index = keys.length - 1; index >= 0; index--) {
|
|
668
|
-
const before = yield* Clock.currentTimeMillis;
|
|
669
|
-
if (before >= writeDeadline || before >= chunkExpiry)
|
|
670
|
-
return false;
|
|
671
|
-
pendingWrite = true;
|
|
672
|
-
yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
673
|
-
pendingWrite = false;
|
|
674
|
-
})));
|
|
675
|
-
if ((yield* Clock.currentTimeMillis) >= writeDeadline)
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
return true;
|
|
679
|
-
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
|
|
680
|
-
if (!written)
|
|
681
|
-
yield* release;
|
|
682
|
-
else
|
|
683
|
-
yield* completeStash(settle);
|
|
684
|
-
return written;
|
|
685
|
-
}), this.opts);
|
|
589
|
+
return runOnPartition(registryEdgeProgram(Registry.prototype, this, "stashResult", id, chunks, ttlSeconds, partition), this.opts);
|
|
686
590
|
}
|
|
687
591
|
/**
|
|
688
592
|
* Storage namespaced to the root result partition, kept separate from any
|
|
@@ -691,7 +595,7 @@ export class Registry {
|
|
|
691
595
|
resultIdentity() {
|
|
692
596
|
return { subject: null, principal: null, endpoint: "/mcp", origin: null };
|
|
693
597
|
}
|
|
694
|
-
recheckResultAccess(
|
|
598
|
+
recheckResultAccess(_dependencies, _classification, _signal) {
|
|
695
599
|
return Promise.resolve(true);
|
|
696
600
|
}
|
|
697
601
|
resultsStorage() {
|
|
@@ -700,11 +604,8 @@ export class Registry {
|
|
|
700
604
|
credentialUiAvailable() {
|
|
701
605
|
return Boolean(this.opts.credentialUi);
|
|
702
606
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (!state || !principalKey)
|
|
706
|
-
throw new Error("OAuth requires state and an initiating user");
|
|
707
|
-
await this.storeOAuthHandoff(id, state, principalKey);
|
|
607
|
+
bindOAuthHandoff(id, authorizationUrl, principalKey) {
|
|
608
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "bindOAuthHandoff", id, authorizationUrl, principalKey));
|
|
708
609
|
}
|
|
709
610
|
observedOutputSchema(connectorId, definition) {
|
|
710
611
|
return this.observedOutputSchemas.get(connectorId, definition);
|
|
@@ -740,96 +641,73 @@ export class Registry {
|
|
|
740
641
|
...(connector.authScope === "personal" ? { personal: true } : {}),
|
|
741
642
|
}, activityRequest(ctx.requestScope));
|
|
742
643
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (tools.length > MAX_CATALOG_TOOLS ||
|
|
764
|
-
encoder.encode(JSON.stringify(tools)).byteLength > MAX_SERIALIZED_CATALOG_BYTES) {
|
|
765
|
-
throw new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", { retryable: false });
|
|
766
|
-
}
|
|
767
|
-
const facts = frozenFacts(structuredClone(tools));
|
|
768
|
-
const accepted = this.acceptToolNames(facts);
|
|
769
|
-
this.observeDroppedToolNames(connector, facts.length - accepted.length);
|
|
770
|
-
const review = catalogReviewOf(connector);
|
|
771
|
-
if (review)
|
|
772
|
-
await observeReviewedDrift(connector, review, accepted, this.opts.logger);
|
|
773
|
-
if (catalogFetchedAt(ctx) === undefined) {
|
|
774
|
-
try {
|
|
775
|
-
await storeCustomCatalogFallback(ctx, id, facts);
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error));
|
|
779
|
-
}
|
|
780
|
-
try {
|
|
781
|
-
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 });
|
|
782
664
|
}
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
}
|
|
785
671
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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);
|
|
793
680
|
});
|
|
794
|
-
return markCatalogFreshness(facts, freshUntil);
|
|
795
681
|
}
|
|
796
682
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
797
683
|
acceptToolNames(tools) {
|
|
798
684
|
const accepted = tools.filter((tool) => !hasControlCharacters(tool.name));
|
|
799
685
|
return accepted.length === tools.length ? tools : accepted;
|
|
800
686
|
}
|
|
801
|
-
/** Observe intake once, never while serving an older cached listing. */
|
|
802
|
-
observeDroppedToolNames(connector, count) {
|
|
803
|
-
if (count > 0 || this.droppedToolNames.has(connector.id)) {
|
|
804
|
-
this.droppedToolNames.set(connector.id, { count, observedAt: new Date().toISOString() });
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
687
|
/**
|
|
808
688
|
* One connector's catalog as served: the cached downstream facts, classified
|
|
809
|
-
* on every read by
|
|
810
|
-
*
|
|
811
|
-
* stale verdict — from an older review, an older release, or a stale
|
|
812
|
-
* fallback — and nothing a caller or decorator does to a served tool reaches
|
|
813
|
-
* 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.
|
|
814
691
|
*/
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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) {
|
|
829
707
|
const override = this.classification[id]?.[tool.name];
|
|
830
|
-
return {
|
|
708
|
+
return carryToolClassification(tool, {
|
|
831
709
|
...structuredClone(tool),
|
|
832
|
-
classification: classifyTool(tool, override),
|
|
710
|
+
classification: classifyTool(tool, override === "hidden" ? undefined : override),
|
|
833
711
|
...(override !== undefined
|
|
834
712
|
? {
|
|
835
713
|
annotations: {
|
|
@@ -839,139 +717,268 @@ export class Registry {
|
|
|
839
717
|
},
|
|
840
718
|
}
|
|
841
719
|
: {}),
|
|
842
|
-
};
|
|
720
|
+
});
|
|
843
721
|
}
|
|
844
722
|
/** Construction-only view for the secret-free static config description. */
|
|
845
723
|
describeStaticTools(id) {
|
|
846
724
|
const tools = this.connectors.get(id)?.staticTools;
|
|
847
|
-
return tools
|
|
725
|
+
return tools
|
|
726
|
+
?.filter((tool) => this.classification[id]?.[tool.name] !== "hidden")
|
|
727
|
+
.map((tool) => this.publishTool(id, tool));
|
|
848
728
|
}
|
|
849
|
-
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
|
+
}
|
|
850
738
|
const known = new Set(tools.map((tool) => tool.name));
|
|
851
739
|
for (const name of Object.keys(overrides ?? {})) {
|
|
852
|
-
if (
|
|
740
|
+
if (known.has(name))
|
|
741
|
+
continue;
|
|
742
|
+
if (this.connectors.get(id)?.staticTools) {
|
|
853
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`);
|
|
854
749
|
}
|
|
855
750
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
if (!connector)
|
|
859
|
-
throw new Error(`Unknown connector "${id}"`);
|
|
860
|
-
if (!requestScope) {
|
|
861
|
-
return this.loadTools(id, baseUrl, requestScope, callOptions, scope);
|
|
862
|
-
}
|
|
863
|
-
let loads = this.requestCatalogLoads.get(requestScope);
|
|
864
|
-
if (!loads) {
|
|
865
|
-
loads = new Map();
|
|
866
|
-
this.requestCatalogLoads.set(requestScope, loads);
|
|
867
|
-
}
|
|
868
|
-
const requestLoads = loads;
|
|
869
|
-
const key = JSON.stringify([id, scope?.principalKey, scope?.subjectKey, scope?.caller]);
|
|
870
|
-
let load = requestLoads.get(key);
|
|
871
|
-
if (!load) {
|
|
872
|
-
const started = new SharedRead((signal) => this.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, scope),
|
|
873
|
-
// Settled or cancelled, the read leaves the map before any caller
|
|
874
|
-
// resumes, so the next one asks the caches afresh.
|
|
875
|
-
() => {
|
|
876
|
-
if (requestLoads.get(key) === started)
|
|
877
|
-
requestLoads.delete(key);
|
|
878
|
-
if (requestLoads.size === 0) {
|
|
879
|
-
this.requestCatalogLoads.delete(requestScope);
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
requestLoads.set(key, started);
|
|
883
|
-
load = started;
|
|
884
|
-
}
|
|
885
|
-
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));
|
|
886
753
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const vault = this.opts.credentialVault;
|
|
890
|
-
if (!credential || !vault)
|
|
891
|
-
return undefined;
|
|
892
|
-
try {
|
|
893
|
-
const values = await vault.getAll(id, this.opts.credentialOwner);
|
|
894
|
-
const shape = storedCredentialShape(credential, values);
|
|
895
|
-
return shape.state === "mismatch" ? shape.message : undefined;
|
|
896
|
-
}
|
|
897
|
-
catch (error) {
|
|
898
|
-
logFailure(this.opts.logger, "credential shape read failed", failureRecord({ connector: id }, error));
|
|
899
|
-
return undefined;
|
|
900
|
-
}
|
|
754
|
+
credentialDriftFor(id) {
|
|
755
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "credentialDriftFor", id));
|
|
901
756
|
}
|
|
902
757
|
/** Age of a complete cache entry; reads never refresh it. */
|
|
903
758
|
catalogAgeMs(id) {
|
|
904
759
|
const observedAt = this.catalogObservedAt.get(id);
|
|
905
760
|
return observedAt === undefined ? null : Math.max(0, Date.now() - observedAt);
|
|
906
761
|
}
|
|
907
|
-
/**
|
|
908
|
-
|
|
762
|
+
/** Pure lookup shared by status probes and operator catalog refreshes. */
|
|
763
|
+
withCatalogObservations(id, _baseUrl, status, _scope) {
|
|
909
764
|
const connector = this.connectors.get(id);
|
|
910
765
|
if (!connector)
|
|
911
766
|
return ownStatus({ state: "error", message: "Unknown connector" });
|
|
912
|
-
const
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const withObservations = (status) => {
|
|
921
|
-
const report = this.catalogDriftOf(connector);
|
|
922
|
-
const access = this.catalogAccess.get(id);
|
|
923
|
-
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
924
|
-
// a connector cannot smuggle payload through either registry-owned
|
|
925
|
-
// observation when this runtime has not made one, nor through its
|
|
926
|
-
// message, which survives only when connecta wrote it (INV-6).
|
|
927
|
-
return Object.assign(boundedStatus(status), {
|
|
928
|
-
...(report ? { catalogDrift: report } : {}),
|
|
929
|
-
...(access ? { catalogAccess: { ...access } } : {}),
|
|
930
|
-
});
|
|
931
|
-
};
|
|
932
|
-
// Credential declarations own their readiness even when a static catalog
|
|
933
|
-
// or a plugin status would otherwise report success. Never probe an empty slot.
|
|
934
|
-
if (connector.credential) {
|
|
935
|
-
try {
|
|
936
|
-
const values = (await this.opts.credentialVault?.getAll(id, this.opts.credentialOwner)) ?? null;
|
|
937
|
-
const shape = storedCredentialShape(connector.credential, values);
|
|
938
|
-
if (shape.state === "missing")
|
|
939
|
-
return withObservations(ownStatus({ state: "credential_required" }));
|
|
940
|
-
if (shape.state === "mismatch")
|
|
941
|
-
return withObservations(ownStatus({ state: "auth_required" }));
|
|
942
|
-
}
|
|
943
|
-
catch (err) {
|
|
944
|
-
return withObservations(failureStatus(id, err));
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
if (connector.status) {
|
|
948
|
-
try {
|
|
949
|
-
return withObservations(await connector.status(ctx));
|
|
950
|
-
}
|
|
951
|
-
catch (err) {
|
|
952
|
-
return withObservations(failureStatus(id, err));
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
try {
|
|
956
|
-
await this.getTools(id, baseUrl, requestScope, callOptions, scope);
|
|
957
|
-
return withObservations({ state: "ok" });
|
|
958
|
-
}
|
|
959
|
-
catch (err) {
|
|
960
|
-
return withObservations(failureStatus(id, err));
|
|
961
|
-
}
|
|
767
|
+
const access = this.catalogAccess.get(id);
|
|
768
|
+
// Connector.status is an open plugin seam. Rebuild its public fields so
|
|
769
|
+
// it cannot smuggle payload or another partition's observations (INV-6).
|
|
770
|
+
return Object.assign(boundedStatus(status), access ? { catalogAccess: { ...access } } : {});
|
|
771
|
+
}
|
|
772
|
+
/** Best-effort connector status for the operator UI. */
|
|
773
|
+
statusFor(id, baseUrl, requestScope = {}, callOptions = {}, scope) {
|
|
774
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "statusFor", id, baseUrl, requestScope, callOptions, scope));
|
|
962
775
|
}
|
|
963
776
|
/** Rotate the storage generation; in-flight old readers cannot republish it. */
|
|
964
777
|
invalidate(id) {
|
|
965
778
|
void this.invalidateStored(id);
|
|
966
779
|
}
|
|
967
|
-
|
|
968
|
-
this
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
});
|
|
975
982
|
}
|
|
976
983
|
}
|
|
977
984
|
class ScopedRegistryView {
|
|
@@ -1037,27 +1044,8 @@ class ScopedRegistryView {
|
|
|
1037
1044
|
const connector = this.getConnector(parsed.connectorId);
|
|
1038
1045
|
return connector ? { connector, toolName: parsed.toolName } : null;
|
|
1039
1046
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!registry) {
|
|
1043
|
-
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1044
|
-
}
|
|
1045
|
-
const tools = await registry.getTools(args[0], args[1], args[2], args[3], this.scope);
|
|
1046
|
-
const granted = this.scope.toolAccess?.get(args[0]);
|
|
1047
|
-
if (!granted)
|
|
1048
|
-
return tools;
|
|
1049
|
-
// Every consumer — search, describe, call_tool, and a program's
|
|
1050
|
-
// connecta.call — resolves through this list, so an ungranted tool is
|
|
1051
|
-
// indistinguishable from one the connector never had.
|
|
1052
|
-
const visible = tools.filter((tool) => granted.has(tool.name));
|
|
1053
|
-
if (visible.length < granted.size) {
|
|
1054
|
-
const present = new Set(tools.map((tool) => tool.name));
|
|
1055
|
-
for (const name of granted) {
|
|
1056
|
-
if (!present.has(name))
|
|
1057
|
-
this.root.noteAbsentGrant(args[0], name);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
return carryCatalogFreshness(tools, visible);
|
|
1047
|
+
getTools(...args) {
|
|
1048
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "getTools", ...args));
|
|
1061
1049
|
}
|
|
1062
1050
|
contextFor(...args) {
|
|
1063
1051
|
const registry = this.registryFor(args[0]);
|
|
@@ -1083,15 +1071,14 @@ class ScopedRegistryView {
|
|
|
1083
1071
|
origin: this.scope.origin ?? null,
|
|
1084
1072
|
};
|
|
1085
1073
|
}
|
|
1086
|
-
recheckResultAccess(
|
|
1087
|
-
return this.scope.currentResultAccess?.(
|
|
1074
|
+
recheckResultAccess(dependencies, classification, signal) {
|
|
1075
|
+
return this.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true);
|
|
1088
1076
|
}
|
|
1089
1077
|
resultsStorage() {
|
|
1090
1078
|
return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
|
|
1091
1079
|
}
|
|
1092
1080
|
credentialDriftFor(id) {
|
|
1093
|
-
|
|
1094
|
-
return registry ? registry.credentialDriftFor(id) : Promise.resolve(undefined);
|
|
1081
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "credentialDriftFor", id));
|
|
1095
1082
|
}
|
|
1096
1083
|
observedOutputSchema(connectorId, definition) {
|
|
1097
1084
|
return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
|
|
@@ -1103,19 +1090,65 @@ class ScopedRegistryView {
|
|
|
1103
1090
|
return this.registryFor(id)?.catalogAgeMs(id) ?? null;
|
|
1104
1091
|
}
|
|
1105
1092
|
statusFor(...args) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1093
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "statusFor", ...args));
|
|
1094
|
+
}
|
|
1095
|
+
withCatalogObservations(id, baseUrl, status) {
|
|
1096
|
+
return (this.registryFor(id)?.withCatalogObservations(id, baseUrl, status, this.scope) ??
|
|
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
|
}
|