From 61d506868406978e17236daad0c4ddeb9c777f25 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan Date: Tue, 20 Oct 2026 17:35:03 +0530 Subject: [PATCH 1/2] test: freeze response timestamps in byte comparisons --- tests/test_prompt_cache_affinity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_prompt_cache_affinity.py b/tests/test_prompt_cache_affinity.py index 070bfc10..12861f62 100644 --- a/tests/test_prompt_cache_affinity.py +++ b/tests/test_prompt_cache_affinity.py @@ -319,6 +319,7 @@ def send(**kwargs): monkeypatch.setattr(app_module.ProxyService, "probe_nanogpt_key", lambda *args: pytest.fail("unexpected live probe")) from services.protocol_translation import responses as responses_translation monkeypatch.setattr(responses_translation, "new_id", lambda prefix: f"{prefix}_synthetic") + monkeypatch.setattr(responses_translation, "unix_time", lambda: 1_700_000_000) CredentialPool.record("ce-gpt-plus", "synthetic-first", 429) client = app.test_client() headers = {"Authorization": "Bearer admin-test-key", "Session-Id": "conversation"} From da66481f8f435e5e8e7160914f2b3b7ccaefc2d1 Mon Sep 17 00:00:00 2001 From: Abhyudaya Baiju Tharakan Date: Tue, 20 Oct 2026 17:38:13 +0530 Subject: [PATCH 2/2] feat: restore key data controls with verified rollout --- .github/workflows/ci.yml | 8 +- .github/workflows/deploy.yml | 11 +- README.md | 2 + config.py | 1 + docs/deployment-cloudflare.md | 40 +- docs/key-data-guards.md | 189 ++ .../0041_key_data_guards.sql | 3 + .../0042_usage_token_details.sql | 5 + .../0043_integration_data_policy.sql | 3 + package.json | 6 +- providers/data-classes.json | 48 + providers/openai_compatible.py | 5 + routes/auto_routes.py | 2 + routes/cascades.py | 21 +- routes/intelligence.py | 7 + routes/media.py | 6 +- routes/media_narration.py | 3 +- routes/proxy.py | 2 + routes/unified.py | 6 +- routes/usage.py | 6 +- scripts/check_worker_bundle.mjs | 35 + scripts/deploy.sh | 3 + scripts/key_data_preflight.mjs | 90 + services/accounted_dispatch.py | 4 + services/auth_service.py | 23 +- services/cloudflare_ai.py | 4 +- services/control_plane_backup.py | 2 +- services/dispatch_network.py | 55 + services/dispatch_policy.py | 23 + services/gemini_credentials.py | 148 + services/intelligence_transport.py | 10 +- services/key_controls.py | 22 +- services/key_data_guards.py | 126 + services/knowledge_client.py | 7 +- services/media_storage.py | 10 +- services/native_token_count.py | 3 +- services/prompt_cache_cost.py | 7 +- services/provider_assets.py | 37 + services/provider_credentials.py | 100 + services/provider_dispatch.py | 145 + services/proxy_service.py | 110 +- services/request_accounting.py | 8 +- services/shadow_eval_contract.py | 5 +- services/shadow_eval_data_policy.py | 30 + services/shadow_eval_runner.py | 6 +- services/stream_cost_breaker.py | 2 +- services/telemetry_export.py | 7 +- services/usage_store.py | 29 +- services/usage_types.py | 22 +- services/user_store.py | 12 +- services/video_generation.py | 26 +- static/js/key-controls.js | 4 + static/js/usage.js | 6 +- templates/usage.html | 6 +- templates/users.html | 9 +- tests/conftest.py | 33 + tests/dispatch_fixture.py | 20 + tests/fixtures/secret_firewall_coverage.json | 7 +- tests/helpers/dispatch_fixture.mjs | 27 + tests/helpers/load_cloudflare_worker.mjs | 2 + tests/helpers/local_miniflare.mjs | 19 + tests/helpers/network_fixture.mjs | 7 + tests/helpers/network_guard.mjs | 39 + tests/helpers/policy_test.mjs | 10 + tests/helpers/provider_binding.mjs | 28 + tests/helpers/roleplay_fixture.mjs | 3 +- tests/knowledge_fixture.mjs | 3 +- tests/outbound_inventory.json | 2427 +++++++++++++++++ tests/support/account_envelope_driver.py | 66 + tests/support/config_revision_fixtures.mjs | 6 +- tests/test_account_contract_worker.mjs | 21 + tests/test_account_envelope_integration.mjs | 88 + tests/test_ai_outbound_worker.mjs | 10 +- tests/test_app_structure.py | 21 +- tests/test_auto_routes_worker.mjs | 2 +- tests/test_cascades_worker.mjs | 2 +- tests/test_child_safety_worker.mjs | 2 +- tests/test_client_headers_worker.mjs | 21 +- tests/test_cline_pass_provider.py | 6 + tests/test_cloudflare_worker.mjs | 6 +- tests/test_codex_easy_transport.py | 6 + tests/test_config_revision_sync.py | 2 + tests/test_config_revision_sync_worker.mjs | 2 +- tests/test_config_snapshots_worker.mjs | 2 +- tests/test_control_audit_worker.mjs | 8 +- tests/test_control_state_worker.mjs | 2 +- tests/test_control_users_worker.mjs | 29 +- tests/test_d1_integration.mjs | 6 +- tests/test_d1_schema_worker.mjs | 4 +- tests/test_dashboard_scripts.mjs | 2 +- tests/test_dispatch_network.py | 62 + tests/test_dispatch_network_worker.mjs | 109 + tests/test_edge_request_metrics_worker.mjs | 18 +- tests/test_evaluation_noise_floor_worker.mjs | 2 +- tests/test_free_routes_worker.mjs | 3 +- tests/test_gemini_data_guards.py | 122 + tests/test_generation_deadline.py | 5 + tests/test_gpt_image_moderation_worker.mjs | 9 +- tests/test_haiku_intelligence_worker.mjs | 2 +- tests/test_handoff_script.py | 5 +- tests/test_intelligence_auth_worker.mjs | 13 +- tests/test_intelligence_cancellation.py | 3 +- tests/test_intelligence_d1_worker.mjs | 2 +- tests/test_intelligence_operator.mjs | 2 +- tests/test_key_controls.py | 6 +- tests/test_key_data_guards.py | 152 ++ tests/test_key_data_guards_worker.mjs | 85 + tests/test_key_data_intelligence.py | 66 + tests/test_key_data_preflight.mjs | 65 + tests/test_kimi_code_transport.py | 6 + tests/test_knowledge_alexandria.mjs | 2 +- tests/test_knowledge_authority.mjs | 34 +- tests/test_knowledge_corpus.mjs | 19 +- tests/test_knowledge_edge.mjs | 4 +- tests/test_knowledge_handoffs.mjs | 8 +- tests/test_knowledge_ingestion.mjs | 2 +- tests/test_knowledge_keys.mjs | 2 +- tests/test_knowledge_ledger.mjs | 2 +- tests/test_knowledge_memo_latency.mjs | 2 +- tests/test_knowledge_memos.mjs | 11 +- tests/test_knowledge_native.mjs | 2 +- ...test_knowledge_product_site_boundaries.mjs | 2 +- tests/test_knowledge_product_site_latency.mjs | 2 +- tests/test_knowledge_product_site_storage.mjs | 2 +- tests/test_knowledge_product_sites.mjs | 2 +- tests/test_knowledge_provider_runtime.mjs | 14 +- tests/test_knowledge_providers.mjs | 2 +- tests/test_knowledge_retrieval.mjs | 2 +- tests/test_knowledge_routes.py | 17 +- tests/test_knowledge_secret_firewall.mjs | 2 +- tests/test_knowledge_service.mjs | 14 +- tests/test_knowledge_skills.mjs | 12 +- tests/test_knowledge_skills_import.mjs | 23 +- tests/test_knowledge_skills_market.mjs | 7 +- tests/test_knowledge_ui.mjs | 7 +- tests/test_knowledge_workflow_runtime.mjs | 16 +- tests/test_mcp_contract_drift_worker.mjs | 11 +- tests/test_media_files_worker.mjs | 4 +- tests/test_media_generation.py | 5 +- tests/test_media_jobs_worker.mjs | 2 +- tests/test_media_outbound_worker.mjs | 5 +- tests/test_native_token_count.py | 46 +- tests/test_opencode_provider.py | 11 + tests/test_opencode_thinking_worker.mjs | 9 +- tests/test_outbound_inventory.py | 48 + tests/test_payment_billing_worker.mjs | 5 +- tests/test_pii_rehydration_worker.mjs | 12 +- tests/test_prompt_cache_cost_worker.mjs | 5 +- tests/test_prompt_injection_worker.mjs | 16 +- tests/test_prompt_templates_worker.mjs | 2 +- tests/test_provider_assets.py | 128 + tests/test_provider_dispatch.py | 170 ++ tests/test_provider_dispatch_worker.mjs | 151 + tests/test_rate_limit_headers.py | 3 +- tests/test_raw_passthrough.py | 6 + tests/test_realtime_worker.mjs | 74 +- tests/test_request_cancellation_worker.mjs | 4 +- tests/test_retry_advice.py | 5 + tests/test_route_health_worker.mjs | 2 +- tests/test_scim_provisioning_worker.mjs | 2 +- tests/test_secret_firewall.py | 5 + .../test_secret_firewall_coverage_worker.mjs | 3 +- tests/test_secret_firewall_worker.mjs | 2 +- tests/test_shadow_eval.py | 6 +- tests/test_shadow_eval_data_guards.py | 89 + tests/test_shadow_eval_worker.mjs | 2 +- tests/test_shared_generation_cache_worker.mjs | 15 +- tests/test_skills_clients.py | 10 +- tests/test_stream_cost_breaker.py | 2 + tests/test_stream_cost_breaker_worker.mjs | 2 +- tests/test_tenant_hierarchy_worker.mjs | 13 +- tests/test_transport_resilience.py | 6 + tests/test_upstream_outcome.py | 5 + tests/test_usage_ledger.py | 2 +- tests/test_usage_ledger_worker.mjs | 6 +- tests/test_usage_receipts_worker.mjs | 4 +- tests/test_usage_reservations_worker.mjs | 6 +- tests/test_usage_token_details.py | 32 + tests/test_usage_token_details_worker.mjs | 39 + tests/test_user_store_d1.py | 3 +- tests/test_vertex_dispatch.py | 124 + tests/test_wave3_hub_worker.mjs | 20 +- tests/test_wave3_transport_worker.mjs | 3 +- tests/test_wave4_worker.mjs | 13 +- tests/test_wave5_worker.mjs | 25 +- tests/test_wave6_worker.mjs | 25 +- tests/test_wave7_worker.mjs | 15 +- worker/access-sso.mjs | 6 +- worker/account-contract.mjs | 31 + worker/ai-outbound.mjs | 16 +- worker/container-env.mjs | 1 + worker/control-users-d1.mjs | 45 +- worker/dispatch-network.mjs | 32 + worker/dispatch-policy.mjs | 14 + worker/gateway-extensions.mjs | 15 + worker/intelligence-auth-d1.mjs | 9 + worker/key-data-guards.mjs | 75 + worker/knowledge-edge.mjs | 2 +- worker/knowledge-outbound.mjs | 4 +- worker/knowledge/authority.mjs | 8 +- worker/knowledge/corpus.mjs | 17 +- worker/knowledge/index.mjs | 10 +- worker/knowledge/ingestion.mjs | 5 +- worker/knowledge/memos.mjs | 3 +- worker/knowledge/providers/transport.mjs | 5 +- worker/knowledge/service.mjs | 18 +- worker/knowledge/skills-import.mjs | 5 +- worker/knowledge/skills-store.mjs | 9 +- worker/knowledge/skills.mjs | 3 +- worker/media-outbound.mjs | 17 +- worker/native-config-sync.mjs | 1 + worker/provider-dispatch.mjs | 125 + worker/realtime.mjs | 24 +- worker/request-telemetry.mjs | 11 +- worker/roleplay/credential-health.mjs | 6 +- worker/roleplay/transport.mjs | 25 +- worker/scheduled-maintenance.mjs | 5 +- worker/secret-firewall.mjs | 8 +- worker/usage-buckets-d1.mjs | 8 +- worker/usage-ledger-d1.mjs | 11 +- wrangler.jsonc | 2 +- wrangler.knowledge.jsonc | 2 +- 222 files changed, 6661 insertions(+), 518 deletions(-) create mode 100644 docs/key-data-guards.md create mode 100644 intelligence-migrations/0041_key_data_guards.sql create mode 100644 intelligence-migrations/0042_usage_token_details.sql create mode 100644 intelligence-migrations/0043_integration_data_policy.sql create mode 100644 providers/data-classes.json create mode 100644 scripts/check_worker_bundle.mjs create mode 100644 scripts/key_data_preflight.mjs create mode 100644 services/dispatch_network.py create mode 100644 services/dispatch_policy.py create mode 100644 services/gemini_credentials.py create mode 100644 services/key_data_guards.py create mode 100644 services/provider_assets.py create mode 100644 services/provider_credentials.py create mode 100644 services/provider_dispatch.py create mode 100644 services/shadow_eval_data_policy.py create mode 100644 tests/dispatch_fixture.py create mode 100644 tests/helpers/dispatch_fixture.mjs create mode 100644 tests/helpers/local_miniflare.mjs create mode 100644 tests/helpers/network_fixture.mjs create mode 100644 tests/helpers/network_guard.mjs create mode 100644 tests/helpers/policy_test.mjs create mode 100644 tests/helpers/provider_binding.mjs create mode 100644 tests/outbound_inventory.json create mode 100644 tests/support/account_envelope_driver.py create mode 100644 tests/test_account_contract_worker.mjs create mode 100644 tests/test_account_envelope_integration.mjs create mode 100644 tests/test_dispatch_network.py create mode 100644 tests/test_dispatch_network_worker.mjs create mode 100644 tests/test_gemini_data_guards.py create mode 100644 tests/test_key_data_guards.py create mode 100644 tests/test_key_data_guards_worker.mjs create mode 100644 tests/test_key_data_intelligence.py create mode 100644 tests/test_key_data_preflight.mjs create mode 100644 tests/test_outbound_inventory.py create mode 100644 tests/test_provider_assets.py create mode 100644 tests/test_provider_dispatch.py create mode 100644 tests/test_provider_dispatch_worker.mjs create mode 100644 tests/test_shadow_eval_data_guards.py create mode 100644 tests/test_usage_token_details.py create mode 100644 tests/test_usage_token_details_worker.mjs create mode 100644 tests/test_vertex_dispatch.py create mode 100644 worker/account-contract.mjs create mode 100644 worker/dispatch-network.mjs create mode 100644 worker/dispatch-policy.mjs create mode 100644 worker/key-data-guards.mjs create mode 100644 worker/provider-dispatch.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7a14409..9e71474a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,11 +119,15 @@ jobs: run: git diff --exit-code -- static/css/style.css - name: Frontend build contracts run: node --test tests/test_frontend_build.mjs + - name: Check release configuration + run: node scripts/key_data_preflight.mjs --check + # Bundle-only checks cannot attest remote deployment prerequisites. The + # normal Wrangler build command enforces those before a real upload. - name: Validate Knowledge Worker bundle env: WRANGLER_SEND_METRICS: "false" - run: pnpm exec wrangler deploy --config wrangler.knowledge.jsonc --dry-run --outdir /tmp/knowledge-worker + run: node scripts/check_worker_bundle.mjs --config wrangler.knowledge.jsonc --outdir /tmp/knowledge-worker - name: Validate main Worker bundle env: WRANGLER_SEND_METRICS: "false" - run: pnpm exec wrangler deploy --dry-run --containers-rollout=none --outdir /tmp/main-worker + run: node scripts/check_worker_bundle.mjs --config wrangler.jsonc --outdir /tmp/main-worker diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2961c79b..846d128b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -121,6 +121,8 @@ jobs: run: npm install --global "$(node -p "require('./package.json').packageManager")" - name: Install deployment dependencies run: pnpm install --frozen-lockfile + - name: Check release configuration + run: node scripts/key_data_preflight.mjs --check - name: Check migration files run: node scripts/verify_d1_migrations.mjs --check - name: List pending D1 migrations @@ -136,6 +138,11 @@ jobs: run: | pnpm exec wrangler d1 migrations apply "$D1_DATABASE" --remote node scripts/verify_d1_migrations.mjs "$D1_DATABASE" + - name: Check migrated schema and required bindings + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: node scripts/key_data_preflight.mjs --prepare - name: Deploy Knowledge Worker id: knowledge env: @@ -148,7 +155,9 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - run: pnpm exec wrangler deploy --tag "$SHA" --message "$DEPLOY_MESSAGE" + run: | + pnpm exec wrangler deploy --containers-rollout=none --tag "$SHA" --message "$DEPLOY_MESSAGE" + pnpm exec wrangler deploy --tag "$SHA" --message "$DEPLOY_MESSAGE" - name: Verify deployment id: verify env: diff --git a/README.md b/README.md index db492430..ba4f2efb 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,8 @@ SQLite or PostgreSQL elsewhere). Each key can have daily and monthly dollar budg a model allowlist, an expiry time and client address ranges; the dashboard's Usage page and `GET /v1/usage` show spend and remaining budget, and request telemetry can be exported over OTLP. See [usage, budgets and key controls](docs/usage-and-budgets.md). +Keys can also restrict provider data classes and enforce request preferences; see +[key data controls and usage details](docs/key-data-guards.md). Administrators can query `GET /admin/providers/usage` from an authenticated dashboard session to combine provider-account allowances with local request, diff --git a/config.py b/config.py index b4cc27ee..e563c0bf 100644 --- a/config.py +++ b/config.py @@ -66,6 +66,7 @@ def load_env_choice( class Config: + GEMINI_DATA_TIER = os.environ.get("GEMINI_DATA_TIER", "free").strip().lower() FREE_ROUTE_EXTRA_PROVIDERS = os.environ.get('FREE_ROUTE_EXTRA_PROVIDERS', '') FREE_ROUTE_WORKERSAI_ACCOUNT_ID = os.environ.get('FREE_ROUTE_WORKERSAI_ACCOUNT_ID', '') FREE_ROUTE_FREE_TIER_PROVIDERS = os.environ.get('FREE_ROUTE_FREE_TIER_PROVIDERS', '') diff --git a/docs/deployment-cloudflare.md b/docs/deployment-cloudflare.md index 4ab4bfae..8ed55102 100644 --- a/docs/deployment-cloudflare.md +++ b/docs/deployment-cloudflare.md @@ -308,11 +308,12 @@ python -m pytest -q npm run test:worker npm run test:integration npm run cf:migrations:check -npx wrangler deploy --dry-run +node scripts/key_data_preflight.mjs --check +node scripts/check_worker_bundle.mjs --config wrangler.jsonc --outdir /tmp/main-worker ``` Pushing to `main` deploys; see [How deploys work](#how-deploys-work). Never deploy -with a bare `wrangler deploy`, which skips the migrations. +with a bare `wrangler deploy`: its preflight verifies prerequisites but does not apply migrations. ## How deploys work @@ -332,7 +333,10 @@ A push to `main` runs the `ci` workflow. When it passes, the `deploy` workflow first because the main Worker binds it (`KNOWLEDGE_SERVICE`) and may call what the new version adds. The main Worker and its Container go last, and only when the repository variable `DEPLOY_MAIN_WORKER` is `true` (see [Workers Builds](#workers-builds)). - Each version is tagged with the full commit SHA, and its message links to the run. + Before upload, the main build verifies the migrated account schema, required bindings + and secret names, and the private Worker's matching commit tag. Main activation uses + `--containers-rollout=none` first, followed by the Container rollout. Each version + is tagged with the full commit SHA, and its message links to the run. 4. **Verify.** `scripts/deploy_status.mjs --verify` retries for up to ten minutes until no migration is pending, `GET /ready` answers 200 (every D1 table exists and the Container is healthy), the Knowledge Worker's live version carries the commit, @@ -349,8 +353,9 @@ SHA. ## Migrations -Migrations are applied before the code that uses them, and with Workers Builds the new -code can briefly run before them, so the schema must work for the old and the new code: +Migrations are applied before the code that uses them. The main build refuses upload +until they are recorded and the required account columns exist. The schema must +still support old and new code during rollout: - Make only additive changes: new tables, new nullable or defaulted columns, new indexes, using `IF NOT EXISTS`. Add each new table to `REQUIRED_D1_TABLES` in @@ -383,14 +388,20 @@ the code, and nothing ships unless ci passed. 3. Add *Containers Edit* to the API token, and run the `deploy` workflow once from the **Actions** tab to deploy the current head of `main`. -**(b) Workers Builds keeps deploying the main Worker.** Leave `DEPLOY_MAIN_WORKER` unset -(the default) or `false`, and keep the Workers Builds deploy command as -`npx wrangler deploy`; do not change it to `npm run deploy`, which would apply the -migrations and deploy the Knowledge Worker a second time. Actions applies migrations and -deploys the Knowledge Worker after ci passes. Workers Builds starts at push time, so after -a push that adds a migration the new main Worker usually runs for a few minutes before -its tables exist, and `/ready` answers 503 `d1_schema_missing` until they do. Code must -tolerate a missing new table, and a push whose ci fails still ships the main Worker. +**(b) Workers Builds remains connected.** It deploys the main Worker and its +Container independently of the GitHub workflow, which also deploys the private +Knowledge Worker. Keep the default Wrangler build command: its preflight refuses +main upload until migrations and the private Worker from the same commit are +ready. A push before CI completes should fail that build. It does not become a +successful deployment automatically after the workflow finishes: rerun the release +only after prerequisites pass, and avoid competing deploy owners. Do not remove the build +preflight for a release. Prefer option (a) to preserve the full +migrations → private Worker → main Worker → Container order in one workflow. + +The account/control RPC is handled by the main Worker against its own D1 binding; +it does not pass through the private Knowledge service. See +[key data controls](key-data-guards.md#account-contract-and-release-preflight) for +the negotiated account contract and local integration evidence. ## One-time setup @@ -442,7 +453,8 @@ which can show no commit until the next deploy. `npm run deploy` (`scripts/deploy.sh`) runs the same order from your machine: the migration checks, `wrangler d1 migrations apply`, the remote check, the Knowledge -Worker and the main Worker. From a clean checkout it tags both versions with the commit. +Worker, the main Worker with Container rollout disabled, then the Container. +Preflight runs before each main upload. From a clean checkout it tags both versions with the commit. Extra arguments go to the final deploy, for example `npm run deploy -- --containers-rollout immediate` for a one-shot rollout of a changed Container image. Use it only when the workflow cannot run, and never while a deploy run diff --git a/docs/key-data-guards.md b/docs/key-data-guards.md new file mode 100644 index 00000000..12aaad2c --- /dev/null +++ b/docs/key-data-guards.md @@ -0,0 +1,189 @@ +# Key data controls + +For an app whose requests carry personal data, set controls through the existing +administrator-only `PUT /users/{username}/controls` endpoint or the user's Limits +form: + +```json +{ + "official_only": true, + "forced_provider_preferences": {"data_collection": "deny"} +} +``` + +`official_only` defaults to false. A restricted key receives HTTP 403 with +`provider_class_not_allowed` for a direct non-official provider. Auto, intelligence +and cascade routes filter concrete candidates before dispatch, including fallback +and background submissions. No eligible candidate means refusal. Model allowlists +still apply independently. Disabling a control requires an administrator; request +headers and routing overrides cannot disable it. + +`forced_provider_preferences` is an optional JSON object, limited to 4 KiB. It is +merged recursively into the `provider` object on every OpenRouter request made for +the key, including raw passthrough and internally routed requests. Administrator +values win; a caller's `data_collection: "deny"` stays `deny` even if the stored +value says `allow`. Other caller preferences remain unless overridden. See +[provider routing](https://openrouter.ai/docs/guides/routing/provider-selection) +for the supported preference fields. This control does not change another +provider's request format. + +Realtime ticket issuance and socket admission re-read current key data controls. +Restricted keys cannot open non-official models. Realtime refuses any key with +forced preferences, including an empty object, because those preferences cannot +be enforced throughout the socket session. Existing tickets do not bypass a +subsequent control change. + +Requests from keys with either data control are excluded from shadow sampling. +Stored samples recheck their source key before replay and before every candidate +or judge dispatch, including three-arm comparisons. Controlled, inactive, missing +or unverifiable source accounts are skipped; existing samples expire normally. + +Restricted Gemini requests reject caller credential overrides in query parameters, +provider headers or a separate Authorization header. Only the operator's current +`GEMINI_API_KEY` binding is covered by `GEMINI_DATA_TIER=paid`; a missing binding +fails closed, without a cached or legacy-alias fallback. The same restriction +applies to automatic fallback and background dispatch. + +## Provider classes + +`providers/data-classes.json` is shared by Flask and the Worker. Registry adapters +expose `data_class`; classification follows the configured provider namespace, +not a claim in a request or a model's name. Unknown namespaces are conservatively +`reseller`, and unknown `ce-*` namespaces are `subscription`. + +| Class | Provider namespaces | Evidence and scope | +| --- | --- | --- | +| official | openrouter, openai, anthropic, azure | Direct API endpoints in `config.py` and `providers/registry.py`; [router API](https://openrouter.ai/docs/quickstart), [direct model API](https://platform.openai.com/docs/api-reference), [Messages API](https://platform.claude.com/docs/en/api/overview), [Azure inference](https://learn.microsoft.com/en-us/azure/ai-foundry/model-inference/how-to/use-chat-completions). | +| official | groq, together, cerebras, xai | Direct inference service endpoints; [Groq](https://console.groq.com/docs/overview), [Together](https://docs.together.ai/docs/quickstart), [Cerebras](https://inference-docs.cerebras.ai/quickstart), [xAI](https://docs.x.ai/docs/overview). | +| official | scaleway, sambanova, hyperbolic | Configured service APIs; [Scaleway](https://www.scaleway.com/en/docs/generative-apis/quickstart/), [SambaNova](https://docs.sambanova.ai/docs/en/get-started/quickstart), [Hyperbolic](https://docs.hyperbolic.xyz/docs/getting-started). The current Hyperbolic overview emphasizes compute; the historical inference endpoint's availability needs operator confirmation. | +| official | googleai, cloudflare | Cloud-hosted inference endpoints; [Google Cloud](https://cloud.google.com/vertex-ai/docs/start/introduction-unified-platform), [Workers AI](https://developers.cloudflare.com/workers-ai/). | +| official or free | gemini | Defaults to `free`. Set `GEMINI_DATA_TIER=paid` only after confirming every credential used for this namespace belongs to the paid tier. [Pricing and data-use distinctions](https://ai.google.dev/gemini-api/docs/pricing). The proxy cannot discover an account's tier. | +| subscription | ce-gpt-plus, ce-gpt-pro, ce-grok-heavy, ce-claude-kiro, ce-claude-kiro-cheap, ce-claude-max, ce-image, codex-easy | Subscription/pool adapters in `providers/codex_everywhere.py`, `providers/image_relays.py` and the existing README. Pool provenance and current vendor terms remain unverified; these are excluded from restricted keys. | +| subscription | opencode, cline-pass, kimi-code, mimo | Configured coding-plan endpoints, rather than ordinary pay-as-you-go endpoints: [Go](https://opencode.ai/docs/go/), [Cline Pass](https://docs.cline.bot/getting-started/clinepass), [Kimi Code membership](https://www.kimi.com/code/docs/en/kimi-code/membership), [MiMo plan distinction](https://platform.xiaomimimo.com/static/docs/price/pay-as-you-go.md). | +| reseller | aihubmix, navyai, linkapi, nanogpt, a6api, aimlapi, ephone, gguu, gguu-grok, latix | Relay endpoints in `config.py`, `providers/image_relays.py` and the existing README. [Navy's multi-provider API](https://api.navy/docs) also uses subscription billing; the reseller tag takes precedence here. The other relays' current sourcing and data terms remain unverified. | +| reseller | chutes, nineteen | Conservative classification of distributed inference networks: [Chutes](https://chutes.ai/docs/guides/starter-guide), [Nineteen](https://nineteen.ai/app). Whether a separately reviewed deployment should qualify as official remains an operator decision. | +| free | free, gemma, palm | Explicit free pools and conservatively excluded legacy Google aliases; the latter's credential tier is not established by the adapters. | +| free | ollama, vllm, lmstudio, llamacpp, sglang | Operator-configured local inference adapters in `providers/local_inference.py`. This is a conservative exclusion within the four-class vocabulary, not a claim about hosting cost or local data handling. | + +`FREE_ROUTE_FREE_TIER_PROVIDERS` takes precedence over the table, including the +Gemini paid setting. A class describes the integration; it does not certify +retention, training policy, a data-processing agreement, or a custom base URL. +Review operator-supplied endpoints and credentials before admitting traffic. + +## Migration and usage + +Apply `0041_key_data_guards.sql`, `0042_usage_token_details.sql` and +`0043_integration_data_policy.sql` through the tracked migration runner first. +Deploy the matching Worker (including the private Knowledge Worker) next, then +the Container. Enable controls only after both runtimes enforce them. Do not +roll back to a runtime that lacks enforcement while restricted keys are enabled. +Repeat the tracked migration command rather than replaying bare ALTER statements. +An old Worker response, missing column or unsupported policy envelope makes the +new Container refuse generation; it never supplies unrestricted defaults. + +The version-1 `data_policy` envelope requires `version`, `official_only` and +`forced_provider_preferences`, with explicit nulls for unrestricted values. +Bootstrap administrative recovery remains available for management, but cannot +authorise provider dispatch without a current durable account and verified policy. + +`services/provider_dispatch.py` and `worker/provider-dispatch.mjs` are the outbound +provider boundaries. HTTP adapters and SDK/binding operations reread the account +at every dispatch, including retries, fallbacks, video polls, background work, +Realtime, roleplay and Knowledge/MCP. Deferred work carries only identity; it does +not carry a policy snapshot. Stored Knowledge sources and ingestion jobs retain +their source account identity across scheduling, retries and reconciliation. +Imported-skill watches also retain the importing account; legacy imports without +that identity refuse upstream checks until explicitly imported again. +Restricted requests replace cached credentials with +an operator credential freshly resolved for an allowlisted provider API origin, +and reject caller Gemini credentials. Vertex refreshes the current service-account +binding (JSON or file) or the currently selected gcloud account, without using the +legacy process token cache. Missing, changed or malformed bindings refuse dispatch. +Public video assets still require policy admission but never receive API credentials. +Video relay and storage downloads follow at most five HTTPS redirects, rechecking +current policy at each hop. An origin change permanently strips credential headers; +unresolved redirects are errors rather than successful content. +Forced routing preferences merge at the final JSON submission. Realtime and +roleplay refuse forced preferences because their full transports cannot enforce +them. Missing accounts, missing fields, malformed envelopes and store outages +refuse dispatch. The static outbound inventory pins every runtime source module, +independently of HTTP client spelling. Suite-wide runtime guards require an active +network capability at Python's `socket.connect` audit event and Worker fetch/socket +calls, even when clients use the extension socket or tests replace fetch. Provider +SDK and index binding doubles use the shared `providerBinding` fixture, which checks +every method call, nested handle and replacement method. Admission issues a capability only for the transport +operation and revokes it on completion or failure. Mutation tests cover urllib3, +fetch-based adapters, Node HTTP, raw extension sockets and omitted SDK admission wrappers. +Private persistence, storage management, authentication, telemetry, public capability +metadata and webhook delivery are separate control traffic. Their tested network +scopes are destination-bound; local D1 fixtures register only their own disposable +loopback ports. Test gateway clients likewise scope only their local server. + +New columns hold +the two controls and provider-reported `cached_input_tokens` / `reasoning_tokens`. +Existing keys remain unrestricted and existing event details remain null; daily +totals start at zero. Flask adds the same columns to its SQLite or PostgreSQL +ledger. Missing or malformed durable controls fail closed on dispatch. + +Chat Completions and Responses usage-detail objects, including final streaming +usage, populate the counters. Native Messages cache-read and cache-creation input +counts are included in total input, while only cache reads count as cache hits. +Reasoning is a detail of output usage and is not added to output a second time. +Absent details remain unknown on individual events. Recording these counters does +not depend on the optional prompt-cache price-bucket feature. See the router's +[usage accounting](https://openrouter.ai/docs/cookbook/administration/usage-accounting). + +The Usage page shows cache-hit rate and reasoning tokens per key and model. +Cache-hit rate is reported cached input divided by all ledger input in that +group, multiplied by 100; upstreams that omit cache details lower the observed +rate. This is not a request-level cache-hit percentage. No webhook budget alert +is included. + +## Account contract and release preflight + +The Container requests `data_policy_version: 1` on private account RPC operations. +The main Worker owns `intelligence.internal/v1/users` and reads `INTELLIGENCE_DB` +directly. This path does not use `KNOWLEDGE_SERVICE` and needs no additional +policy-signing secret. Both `CONFIG_REVISION_SYNC_ENABLED` and +`STREAM_COST_BREAKER_ENABLED` default off when unset, and the Container receives +explicitly configured flag values through `collectContainerEnv`. + +An earlier Container checks the exact account field set. Sending the three new +policy fields to it produces `account_storage_unavailable`. Version negotiation +therefore preserves its earlier row shape only for accounts whose current policy +is verified and unrestricted. A legacy reader cannot receive a restricted account, +and a legacy writer cannot replace one. A current Container still refuses missing, +malformed or unsupported policy envelopes; it never negotiates down to an older +Worker's unverifiable policy. + +The integration test applies the real migrations through 0043, inserts an account +with realistic metadata and defaulted policy columns, and runs `/v1/models` and +`/v1/chat/completions` through registered Flask routes over HTTP to the main +Worker's real account handler. Only the external provider is mocked. It also +checks current controls, revocation and legacy reads/writes. Matching current +runtimes succeed with both flags unset; this does not establish the exact version +combination or error cause in a historical deployment without its logs or +Container fingerprint. + +The default Wrangler build runs `scripts/key_data_preflight.mjs --deploy` before +upload. It checks the D1 binding and migration ledger, actual policy columns, +Container binding, required secret **names**, and a fully active private Worker +tagged with the same commit. Missing or unreadable prerequisites stop deployment. +It does not fetch secret values. The orchestrated deploy also runs `--prepare` +after migrations and before updating the private Worker, so missing main bindings +stop that update too. `--check` validates local configuration only; +CI uses `scripts/check_worker_bundle.mjs` after that check. The wrapper removes +the release build command only from a temporary config and always passes +`--dry-run`; it cannot upload or attest live readiness. + +Workers Builds can deploy the main Worker and Container without waiting for the +GitHub deploy workflow. With the default build gate it must fail until migrations +and the matching private Worker are ready. Prefer one deployment owner: disable +automatic promotion from Workers Builds and let the CI-gated workflow orchestrate +the release. The manual script and workflow apply tracked migrations, deploy the +private Worker, activate the main Worker with `--containers-rollout=none`, then +roll out the Container from the same commit. Verify both runtime fingerprints and +test an ordinary durable key before enabling controls. Keep controls disabled +through mixed-version rollout; do not roll back to unenforcing runtimes once +restricted keys are enabled. Already-applied migrations stay recorded and are +not replayed as bare ALTER statements. diff --git a/intelligence-migrations/0041_key_data_guards.sql b/intelligence-migrations/0041_key_data_guards.sql new file mode 100644 index 00000000..31571653 --- /dev/null +++ b/intelligence-migrations/0041_key_data_guards.sql @@ -0,0 +1,3 @@ +-- Key data controls. Apply before the matching release. +ALTER TABLE control_users ADD COLUMN official_only INTEGER CHECK (official_only IN (0, 1)); +ALTER TABLE control_users ADD COLUMN forced_provider_preferences TEXT; diff --git a/intelligence-migrations/0042_usage_token_details.sql b/intelligence-migrations/0042_usage_token_details.sql new file mode 100644 index 00000000..c5416980 --- /dev/null +++ b/intelligence-migrations/0042_usage_token_details.sql @@ -0,0 +1,5 @@ +-- Provider-reported usage details; old event rows keep unknown values. +ALTER TABLE usage_events ADD COLUMN cached_input_tokens INTEGER CHECK (cached_input_tokens >= 0); +ALTER TABLE usage_events ADD COLUMN reasoning_tokens INTEGER CHECK (reasoning_tokens >= 0); +ALTER TABLE usage_daily ADD COLUMN cached_input_tokens INTEGER NOT NULL DEFAULT 0 CHECK (cached_input_tokens >= 0); +ALTER TABLE usage_daily ADD COLUMN reasoning_tokens INTEGER NOT NULL DEFAULT 0 CHECK (reasoning_tokens >= 0); diff --git a/intelligence-migrations/0043_integration_data_policy.sql b/intelligence-migrations/0043_integration_data_policy.sql new file mode 100644 index 00000000..76eecd9c --- /dev/null +++ b/intelligence-migrations/0043_integration_data_policy.sql @@ -0,0 +1,3 @@ +-- Integration credentials use the same explicit data controls as account keys. +ALTER TABLE intelligence_principals ADD COLUMN official_only INTEGER CHECK (official_only IS NULL OR official_only IN (0, 1)); +ALTER TABLE intelligence_principals ADD COLUMN forced_provider_preferences TEXT; diff --git a/package.json b/package.json index f1264d01..fffe3c5c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "ruff check . --select E9,F63,F7,F82", "typecheck": "mypy --ignore-missing-imports --follow-imports=skip --disable-error-code import-untyped --disable-error-code var-annotated --disable-error-code arg-type --disable-error-code assignment --disable-error-code operator app.py routes services providers streaming", "test": "python -m pytest -q", - "test:worker": "node --test tests/test_cloudflare_worker.mjs tests/test_child_safety_worker.mjs tests/test_child_safety_dashboard.mjs tests/test_runtime_feature_flags_worker.mjs tests/test_anthropic_worker.mjs tests/test_haiku_intelligence_worker.mjs tests/test_cascades_worker.mjs tests/test_cascades_dashboard.mjs tests/test_image_quality_worker.mjs tests/test_secret_scan_worker.mjs tests/test_secret_firewall_worker.mjs tests/test_secret_firewall_coverage_worker.mjs tests/test_roleplay_compaction_budget_worker.mjs tests/test_roleplay_compaction_model_worker.mjs tests/test_roleplay_stream_validation_worker.mjs tests/test_aihubmix_worker.mjs tests/test_gpt_image_moderation_worker.mjs tests/test_dashboard_scripts.mjs tests/test_operations_dashboard.mjs tests/test_opencode_reasoning_worker.mjs tests/test_opencode_thinking_worker.mjs tests/test_nanogpt_reasoning_worker.mjs tests/test_nanogpt_speed_routing_worker.mjs tests/test_sse_heartbeat_worker.mjs tests/test_roleplay_worker.mjs tests/test_roleplay_config_worker.mjs tests/test_roleplay_glm_failover_worker.mjs tests/test_roleplay_intelligence_worker.mjs tests/test_roleplay_memory_worker.mjs tests/test_roleplay_compatibility_worker.mjs tests/test_roleplay_history_reconciliation_worker.mjs tests/test_roleplay_output_contract_analysis.mjs tests/test_roleplay_output_contract_cleanup_worker.mjs tests/test_roleplay_output_contract_nonstream_worker.mjs tests/test_roleplay_output_contract_worker.mjs tests/test_roleplay_performance_worker.mjs tests/test_roleplay_quality_routing_worker.mjs tests/test_roleplay_reasoning_worker.mjs tests/test_roleplay_session_storage_worker.mjs tests/test_roleplay_transport_failure_worker.mjs tests/test_roleplay_unlimited_worker.mjs tests/test_roleplay_workbench_worker.mjs tests/test_roleplay_candidate_context_worker.mjs tests/test_roleplay_operator_worker.mjs tests/test_workbench_stream.mjs tests/test_intelligence_worker.mjs tests/test_intelligence_d1_worker.mjs tests/test_intelligence_auth_worker.mjs tests/test_intelligence_outbound_worker.mjs tests/test_intelligence_operator.mjs tests/test_control_users_worker.mjs tests/test_control_audit_worker.mjs tests/test_access_sso_worker.mjs tests/test_d1_schema_worker.mjs tests/test_shadow_eval_worker.mjs tests/test_auto_routes_worker.mjs tests/test_ai_outbound_worker.mjs tests/test_knowledge_*.mjs tests/test_control_state_worker.mjs tests/test_route_health_worker.mjs tests/test_gateway_mcp_worker.mjs tests/test_usage_ledger_worker.mjs tests/test_media_outbound_worker.mjs tests/test_media_files_worker.mjs tests/test_media_jobs_worker.mjs tests/test_tool_repair_worker.mjs tests/test_prometheus_worker.mjs tests/test_evaluation_noise_floor_worker.mjs tests/test_prompt_templates_worker.mjs tests/test_config_snapshots_worker.mjs tests/test_managed_idempotency_worker.mjs tests/test_wave3_hub_worker.mjs tests/test_wave3_transport_worker.mjs tests/test_admission_leases_worker.mjs tests/test_edge_request_metrics_worker.mjs tests/test_request_cancellation_worker.mjs tests/test_mcp_contract_drift_worker.mjs tests/test_zero_content_retention_worker.mjs tests/test_prompt_cache_cost_worker.mjs tests/test_config_revision_sync_worker.mjs tests/test_gateway_alerts_worker.mjs tests/test_session_tiers_worker.mjs tests/test_usage_reservations_worker.mjs tests/test_shared_generation_cache_worker.mjs tests/test_deferred_tools_worker.mjs tests/test_generation_deadline_worker.mjs tests/test_wave4_worker.mjs tests/test_responses_state_worker.mjs tests/test_gateway_batches_worker.mjs tests/test_semantic_generation_cache_worker.mjs tests/test_canary_traffic_worker.mjs tests/test_context_pages_worker.mjs tests/test_observability_exports_worker.mjs tests/test_prompt_injection_worker.mjs tests/test_pii_rehydration_worker.mjs tests/test_wave5_worker.mjs tests/test_usage_receipts_worker.mjs tests/test_learned_cooldown_worker.mjs tests/test_stream_cost_breaker_worker.mjs tests/test_realtime_worker.mjs tests/test_latency_slo_worker.mjs tests/test_context_canary_worker.mjs tests/test_enterprise_contracts_worker.mjs tests/test_wave6_worker.mjs tests/test_wave7_worker.mjs tests/test_payment_billing_worker.mjs tests/test_credits_ledger_worker.mjs tests/test_scim_provisioning_worker.mjs tests/test_saml_federation_worker.mjs tests/test_tenant_governance_worker.mjs tests/test_tenant_hierarchy_worker.mjs", + "test:worker": "node --import ./tests/helpers/network_guard.mjs --test tests/test_cloudflare_worker.mjs tests/test_child_safety_worker.mjs tests/test_child_safety_dashboard.mjs tests/test_runtime_feature_flags_worker.mjs tests/test_anthropic_worker.mjs tests/test_haiku_intelligence_worker.mjs tests/test_cascades_worker.mjs tests/test_cascades_dashboard.mjs tests/test_image_quality_worker.mjs tests/test_secret_scan_worker.mjs tests/test_secret_firewall_worker.mjs tests/test_secret_firewall_coverage_worker.mjs tests/test_roleplay_compaction_budget_worker.mjs tests/test_roleplay_compaction_model_worker.mjs tests/test_roleplay_stream_validation_worker.mjs tests/test_aihubmix_worker.mjs tests/test_gpt_image_moderation_worker.mjs tests/test_dashboard_scripts.mjs tests/test_operations_dashboard.mjs tests/test_opencode_reasoning_worker.mjs tests/test_opencode_thinking_worker.mjs tests/test_nanogpt_reasoning_worker.mjs tests/test_nanogpt_speed_routing_worker.mjs tests/test_sse_heartbeat_worker.mjs tests/test_roleplay_worker.mjs tests/test_roleplay_config_worker.mjs tests/test_roleplay_glm_failover_worker.mjs tests/test_roleplay_intelligence_worker.mjs tests/test_roleplay_memory_worker.mjs tests/test_roleplay_compatibility_worker.mjs tests/test_roleplay_history_reconciliation_worker.mjs tests/test_roleplay_output_contract_analysis.mjs tests/test_roleplay_output_contract_cleanup_worker.mjs tests/test_roleplay_output_contract_nonstream_worker.mjs tests/test_roleplay_output_contract_worker.mjs tests/test_roleplay_performance_worker.mjs tests/test_roleplay_quality_routing_worker.mjs tests/test_roleplay_reasoning_worker.mjs tests/test_roleplay_session_storage_worker.mjs tests/test_roleplay_transport_failure_worker.mjs tests/test_roleplay_unlimited_worker.mjs tests/test_roleplay_workbench_worker.mjs tests/test_roleplay_candidate_context_worker.mjs tests/test_roleplay_operator_worker.mjs tests/test_workbench_stream.mjs tests/test_intelligence_worker.mjs tests/test_intelligence_d1_worker.mjs tests/test_intelligence_auth_worker.mjs tests/test_intelligence_outbound_worker.mjs tests/test_intelligence_operator.mjs tests/test_control_users_worker.mjs tests/test_control_audit_worker.mjs tests/test_access_sso_worker.mjs tests/test_d1_schema_worker.mjs tests/test_shadow_eval_worker.mjs tests/test_auto_routes_worker.mjs tests/test_ai_outbound_worker.mjs tests/test_knowledge_*.mjs tests/test_control_state_worker.mjs tests/test_route_health_worker.mjs tests/test_gateway_mcp_worker.mjs tests/test_usage_ledger_worker.mjs tests/test_media_outbound_worker.mjs tests/test_media_files_worker.mjs tests/test_media_jobs_worker.mjs tests/test_tool_repair_worker.mjs tests/test_prometheus_worker.mjs tests/test_evaluation_noise_floor_worker.mjs tests/test_prompt_templates_worker.mjs tests/test_config_snapshots_worker.mjs tests/test_managed_idempotency_worker.mjs tests/test_wave3_hub_worker.mjs tests/test_wave3_transport_worker.mjs tests/test_admission_leases_worker.mjs tests/test_edge_request_metrics_worker.mjs tests/test_request_cancellation_worker.mjs tests/test_mcp_contract_drift_worker.mjs tests/test_zero_content_retention_worker.mjs tests/test_prompt_cache_cost_worker.mjs tests/test_config_revision_sync_worker.mjs tests/test_gateway_alerts_worker.mjs tests/test_session_tiers_worker.mjs tests/test_usage_reservations_worker.mjs tests/test_shared_generation_cache_worker.mjs tests/test_deferred_tools_worker.mjs tests/test_generation_deadline_worker.mjs tests/test_wave4_worker.mjs tests/test_responses_state_worker.mjs tests/test_gateway_batches_worker.mjs tests/test_semantic_generation_cache_worker.mjs tests/test_canary_traffic_worker.mjs tests/test_context_pages_worker.mjs tests/test_observability_exports_worker.mjs tests/test_prompt_injection_worker.mjs tests/test_pii_rehydration_worker.mjs tests/test_wave5_worker.mjs tests/test_usage_receipts_worker.mjs tests/test_learned_cooldown_worker.mjs tests/test_stream_cost_breaker_worker.mjs tests/test_realtime_worker.mjs tests/test_latency_slo_worker.mjs tests/test_context_canary_worker.mjs tests/test_enterprise_contracts_worker.mjs tests/test_wave6_worker.mjs tests/test_wave7_worker.mjs tests/test_payment_billing_worker.mjs tests/test_credits_ledger_worker.mjs tests/test_scim_provisioning_worker.mjs tests/test_saml_federation_worker.mjs tests/test_tenant_governance_worker.mjs tests/test_tenant_hierarchy_worker.mjs tests/test_key_data_guards_worker.mjs tests/test_usage_token_details_worker.mjs tests/test_provider_dispatch_worker.mjs tests/test_dispatch_network_worker.mjs tests/test_frontend_build.mjs tests/test_deploy_pipeline.mjs tests/test_free_routes_worker.mjs tests/test_client_headers_worker.mjs tests/test_key_data_preflight.mjs tests/test_account_contract_worker.mjs", "db:migrate": "python scripts/validate_sqlite_schema.py --apply", "db:validate": "python scripts/validate_sqlite_schema.py", "security:secrets": "python scripts/check_static_secrets.py", @@ -17,8 +17,8 @@ "cf:deploy": "bash scripts/deploy.sh", "cf:dry-run": "wrangler deploy --dry-run", "cf:tail": "wrangler tail", - "test:knowledge": "node --test tests/test_knowledge_*.mjs", - "test:integration": "node --test tests/test_d1_integration.mjs tests/test_config_revision_sync_integration.mjs", + "test:knowledge": "node --import ./tests/helpers/network_guard.mjs --test tests/test_knowledge_*.mjs", + "test:integration": "node --import ./tests/helpers/network_guard.mjs --test tests/test_d1_integration.mjs tests/test_config_revision_sync_integration.mjs tests/test_account_envelope_integration.mjs", "cf:knowledge:dry-run": "wrangler deploy --config wrangler.knowledge.jsonc --dry-run", "cf:knowledge:deploy": "wrangler deploy --config wrangler.knowledge.jsonc", "cf:migrations:check": "node scripts/verify_d1_migrations.mjs --check", diff --git a/providers/data-classes.json b/providers/data-classes.json new file mode 100644 index 00000000..c1ea4dd1 --- /dev/null +++ b/providers/data-classes.json @@ -0,0 +1,48 @@ +{ + "a6api": "reseller", + "aihubmix": "reseller", + "aimlapi": "reseller", + "anthropic": "official", + "azure": "official", + "ce-claude-kiro": "subscription", + "ce-claude-kiro-cheap": "subscription", + "ce-claude-max": "subscription", + "ce-gpt-plus": "subscription", + "ce-gpt-pro": "subscription", + "ce-grok-heavy": "subscription", + "ce-image": "subscription", + "cerebras": "official", + "chutes": "reseller", + "cline-pass": "subscription", + "cloudflare": "official", + "codex-easy": "subscription", + "ephone": "reseller", + "free": "free", + "gemini": "free", + "gemma": "free", + "gguu": "reseller", + "gguu-grok": "reseller", + "googleai": "official", + "groq": "official", + "hyperbolic": "official", + "kimi-code": "subscription", + "latix": "reseller", + "linkapi": "reseller", + "llamacpp": "free", + "lmstudio": "free", + "mimo": "subscription", + "nanogpt": "reseller", + "navyai": "reseller", + "nineteen": "reseller", + "ollama": "free", + "openai": "official", + "opencode": "subscription", + "openrouter": "official", + "palm": "free", + "sambanova": "official", + "scaleway": "official", + "sglang": "free", + "together": "official", + "vllm": "free", + "xai": "official" +} diff --git a/providers/openai_compatible.py b/providers/openai_compatible.py index ae98ac4d..838aaa85 100644 --- a/providers/openai_compatible.py +++ b/providers/openai_compatible.py @@ -21,6 +21,11 @@ class OpenAICompatibleAdapter: chat_path: str = "v1/chat/completions" provider_capabilities: ProviderCapabilities = field(default_factory=ProviderCapabilities) + @property + def data_class(self) -> str: + from services.key_data_guards import provider_data_class + return provider_data_class(self.name) + def chat_completions_url(self) -> str: return f"{self.base_url.rstrip('/')}/{self.chat_path.lstrip('/')}" diff --git a/routes/auto_routes.py b/routes/auto_routes.py index 69f42372..026c4eab 100644 --- a/routes/auto_routes.py +++ b/routes/auto_routes.py @@ -179,6 +179,8 @@ def dispatch_auto_route( canary = canary_traffic.prepare_request(route, payload) candidates = canary.dispatch_order if canary is not None else route.candidates + from services.key_data_guards import eligible_models + candidates = tuple(eligible_models(candidates, getattr(g, "authenticated_user", None) or {})) order = RouteHealth.order(route.id, candidates, circuit_state=_circuit_state) if canary is not None: canary.observe(order.candidates) diff --git a/routes/cascades.py b/routes/cascades.py index a4f91df9..e453dec5 100644 --- a/routes/cascades.py +++ b/routes/cascades.py @@ -177,6 +177,11 @@ def dispatch_cascade(payload, dispatch, *, timeout=120): user = getattr(g, "authenticated_user", None) or {} if not model_allowed(user, config["name"]): raise APIError("This API key is not allowed to use this cascade", 403, {"error": "model_not_allowed"}) + from services.key_data_guards import model_permitted + tiers = [tier for tier in config["tiers"] if model_permitted(user, tier["model"])] + if not tiers: + raise APIError("No eligible provider data class remains", 403, {"error": "provider_class_not_allowed"}) + config = {**config, "tiers": tiers} if "routing" in payload: raise APIError("Cascades do not accept intelligence routing overrides", 400) if payload.get("n", 1) != 1: @@ -284,7 +289,13 @@ def validate_cascade_target(app, auth, model, proxy): config = CascadeService.get_route(model) if config is None: raise APIError(f"Cascade not found: {model}", 404) - for tier in config["tiers"]: + from services.key_data_guards import model_permitted + user = getattr(g, "authenticated_user", None) or {} + tiers = [tier for tier in config["tiers"] if model_permitted(user, tier["model"])] + if not tiers: + raise APIError("No eligible provider data class remains", 403, {"error": "provider_class_not_allowed"}) + denied = 0 + for tier in tiers: try: if tier["model"] == "auto:intelligence": from routes.intelligence import load_policy @@ -292,6 +303,7 @@ def validate_cascade_target(app, auth, model, proxy): if any(model_allowed(getattr(g, "authenticated_user", {}) or {}, candidate["model"]) for candidate in policy["candidates"]): return "cascade" + denied += int(bool(user.get("official_only"))) continue if tier["model"].startswith("free:"): from services.free_model_policy import FREE_MODELS @@ -300,8 +312,13 @@ def validate_cascade_target(app, auth, model, proxy): continue validate_unified_chat_target(app, auth, tier["model"], proxy) return "cascade" - except (APIError, ValueError, GatewayError): + except APIError as error: + denied += int((error.payload or {}).get("error") == "provider_class_not_allowed") + continue + except (ValueError, GatewayError): continue + if denied == len(tiers): + raise APIError("No eligible provider data class remains", 403, {"error": "provider_class_not_allowed"}) raise APIError(f"No configured provider is available for cascade: {model}", 503) diff --git a/routes/intelligence.py b/routes/intelligence.py index 8059dffb..0228f7f6 100644 --- a/routes/intelligence.py +++ b/routes/intelligence.py @@ -168,6 +168,13 @@ def dispatch_intelligence_chat(app, auth, metrics, proxy, payload): policy = {**policy, "candidates": [candidate for candidate in policy["candidates"] if judge_candidate_allowed(candidate["model"])]} + from services.key_data_guards import model_permitted + user = getattr(g, "authenticated_user", None) or {} + if user.get("official_only"): + policy["candidates"] = [candidate for candidate in policy["candidates"] + if model_permitted(user, candidate["model"])] + if not policy["candidates"]: + raise GatewayError("provider_class_not_allowed", "No eligible provider data class remains.", 403) cascade_deadline = getattr(g, "cascade_deadline", None) if cascade_deadline is not None: from services.key_controls import model_allowed diff --git a/routes/media.py b/routes/media.py index 87037629..53af564b 100644 --- a/routes/media.py +++ b/routes/media.py @@ -206,7 +206,9 @@ def create(candidate: str): try: return video_generation.create_job(provider, provider_model, video_request, key, base_url, owner) except APIError as error: - if error.status_code in _PRE_GENERATION_STATUSES: + if error.status_code in _PRE_GENERATION_STATUSES and (error.payload or {}).get("error") not in { + "provider_credential_not_allowed", "attested_credential_unavailable", + "key_data_controls_unavailable", "provider_class_not_allowed"}: raise AutoRouteCandidateUnavailable(error.message) from error raise @@ -282,7 +284,7 @@ def video_content(job_id: str): pass key, base_url = video_credentials(job["p"]) url, headers = video_generation.content_source(job, key, base_url) - return video_generation.stream_content(url, headers) + return video_generation.stream_content(url, headers, provider=job["p"]) @app.route("/v1/media/providers", methods=["GET", "OPTIONS"]) @csrf.exempt diff --git a/routes/media_narration.py b/routes/media_narration.py index b4e5ab62..2910a583 100644 --- a/routes/media_narration.py +++ b/routes/media_narration.py @@ -19,6 +19,7 @@ import re import time from collections.abc import Callable +from services.provider_dispatch import capture_dispatch from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass @@ -295,7 +296,7 @@ def run(self) -> dict: break if shots: with ThreadPoolExecutor(max_workers=min(SHOT_PARALLELISM, len(shots))) as pool: - futures = [pool.submit(copy_current_request_context(lambda shot=shot: self.shot(shot, model))) + futures = [pool.submit(capture_dispatch(copy_current_request_context(lambda shot=shot: self.shot(shot, model)))) for shot in shots] for shot, future in zip(shots, futures, strict=True): entries[shot.index] = future.result() diff --git a/routes/proxy.py b/routes/proxy.py index c3a20a64..8ff05fd6 100644 --- a/routes/proxy.py +++ b/routes/proxy.py @@ -140,6 +140,8 @@ def proxy(api_provider: str, path: str = ""): ): api_provider = "kimi-code" raise APIError("Invalid Kimi Code path", status_code=400) + from services.key_data_guards import require_provider + require_provider(api_provider) if api_provider not in app.config["API_BASE_URLS"]: raise APIError(f"Unsupported API provider: {api_provider}", status_code=400) if api_provider == "linkapi" and any(delimiter in path for delimiter in "?#"): diff --git a/routes/unified.py b/routes/unified.py index 662cbd30..529821af 100644 --- a/routes/unified.py +++ b/routes/unified.py @@ -101,6 +101,8 @@ def _resolve_enabled_model(app, model_id: str): + from services.key_data_guards import require_provider + require_provider(model_id.split(":", 1)[0]) check_managed_candidate(model_id) provider, provider_model = ModelRegistry.parse_model_id(model_id) adapter = get_adapter(provider, app.config["API_BASE_URLS"]) @@ -269,7 +271,9 @@ def validate_unified_chat_target( route = AutoRouteService.get_route(model_id) if route is None: raise APIError(f"Auto route not found: {model_id}", status_code=404) - for candidate in route.candidates: + from services.key_data_guards import eligible_models + eligible = eligible_models(route.candidates, getattr(g, "authenticated_user", None) or {}) + for candidate in eligible: try: _validate_direct_chat_target( app, diff --git a/routes/usage.py b/routes/usage.py index 8cd57ab3..e30bfda7 100644 --- a/routes/usage.py +++ b/routes/usage.py @@ -34,7 +34,7 @@ def _range(days: int) -> tuple[str, str]: def _combined(rows: list) -> dict: - totals = {name: 0 for name in usage_store.TOTAL_COLUMNS[:7]} + totals = {name: 0 for name in usage_store.SUMMARY_COLUMNS} buckets = [0] * len(usage_store.BUCKET_COLUMNS) for row in rows: for name in totals: @@ -80,7 +80,7 @@ def api_usage(): if workspace: scoped = workspace["workspace"] return jsonify({"object": "usage", "principal": principal, "key_prefix": user.get("api_key_prefix"), - "controls": {name: controls[name] for name in ("allowed_models", "allowed_ips", "expires_at")}, + "controls": {name: controls[name] for name in ("allowed_models", "allowed_ips", "expires_at", "official_only", "forced_provider_preferences")}, "budget": {"daily_budget_usd": user.get("daily_budget_usd"), "monthly_budget_usd": user.get("monthly_budget_usd")}, "history_available": True, "range": scoped["range"], "daily": scoped["daily"], "models": scoped["models"], @@ -92,7 +92,7 @@ def api_usage(): "principal": principal, "key_prefix": user.get("api_key_prefix"), "budget": BudgetService.status(user), - "controls": {name: controls[name] for name in ("allowed_models", "allowed_ips", "expires_at")}, + "controls": {name: controls[name] for name in ("allowed_models", "allowed_ips", "expires_at", "official_only", "forced_provider_preferences")}, **usage_history(principal, _days()), **workspace, }) diff --git a/scripts/check_worker_bundle.mjs b/scripts/check_worker_bundle.mjs new file mode 100644 index 00000000..4d1cd53c --- /dev/null +++ b/scripts/check_worker_bundle.mjs @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/** Bundle validation without remote release admission. This command cannot deploy. */ +import { execFileSync } from "node:child_process"; +import { readFileSync, writeFileSync, unlinkSync } from "node:fs"; +import { resolve } from "node:path"; +import { randomUUID } from "node:crypto"; +import { parseArgs } from "node:util"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const ROOT = new URL("../", import.meta.url); +export function bundleConfig(config) { + const result = structuredClone(config); + delete result.build; + return result; +} +export function bundleArguments(config, outdir, main) { + return ["exec", "wrangler", "deploy", "--config", resolve(config), "--dry-run", + ...(main ? ["--containers-rollout=none"] : []), "--outdir", resolve(outdir)]; +} +function main() { + const { values } = parseArgs({ options: { config: { type: "string" }, outdir: { type: "string" } } }); + if (!["wrangler.jsonc", "wrangler.knowledge.jsonc"].includes(values.config) || !values.outdir) { + throw new Error("Pass --config wrangler.jsonc|wrangler.knowledge.jsonc and --outdir"); + } + const source = JSON.parse(readFileSync(new URL(values.config, ROOT))); + // Keep relative paths rooted beside the real configuration. Never change the + // committed release config or provide a general build-admission bypass flag. + const temporary = new URL(`.wrangler-bundle-${randomUUID()}.jsonc`, ROOT); + writeFileSync(temporary, JSON.stringify(bundleConfig(source)), { flag: "wx", mode: 0o600 }); + try { + execFileSync("pnpm", bundleArguments(fileURLToPath(temporary), values.outdir, values.config === "wrangler.jsonc"), + { cwd: ROOT, stdio: "inherit", env: { ...process.env, WRANGLER_SEND_METRICS: "false" } }); + } finally { unlinkSync(temporary); } +} +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 3ecb8ad5..1f368b9a 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -18,8 +18,11 @@ if commit="$(git rev-parse HEAD 2>/dev/null)"; then fi fi +node scripts/key_data_preflight.mjs --check node scripts/verify_d1_migrations.mjs --check npx --no-install wrangler d1 migrations apply "$DATABASE" --remote node scripts/verify_d1_migrations.mjs "$DATABASE" +node scripts/key_data_preflight.mjs --prepare npx --no-install wrangler deploy --config wrangler.knowledge.jsonc ${annotations[@]+"${annotations[@]}"} +npx --no-install wrangler deploy ${annotations[@]+"${annotations[@]}"} --containers-rollout=none npx --no-install wrangler deploy ${annotations[@]+"${annotations[@]}"} "$@" diff --git a/scripts/key_data_preflight.mjs b/scripts/key_data_preflight.mjs new file mode 100644 index 00000000..b80cbc93 --- /dev/null +++ b/scripts/key_data_preflight.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** Release admission, before uploading a Worker or replacing its Container. */ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; +import { localMigrations, remoteAppliedMigrations } from "./verify_d1_migrations.mjs"; + +const ROOT = new URL("../", import.meta.url); +const REQUIRED_SECRETS = ["ADMIN_API_KEY", "FLASK_SECRET_KEY", "JWT_SECRET"]; +const REQUIRED_COLUMNS = ["official_only", "forced_provider_preferences", "max_stream_cost_microusd"]; +class PreflightError extends Error {} +const requireThat = (condition, message) => { if (!condition) throw new PreflightError(message); }; + +export function checkConfig(main, knowledge) { + const database = config => config.d1_databases?.find(item => item.binding === "INTELLIGENCE_DB"); + const first = database(main), second = database(knowledge); + requireThat(main.vars?.AUTH_STORAGE_BACKEND === "d1", "AUTH_STORAGE_BACKEND must explicitly select d1"); + requireThat(first?.database_id && first.database_name && first.migrations_dir === "intelligence-migrations", + "Main Worker needs the migrated INTELLIGENCE_DB binding"); + requireThat(second?.database_id === first.database_id && second?.database_name === first.database_name + && second?.migrations_dir === first.migrations_dir, "Both Workers must bind the same migrated D1"); + requireThat(main.services?.some(item => item.binding === "KNOWLEDGE_SERVICE" && item.service === knowledge.name), + "KNOWLEDGE_SERVICE must target the configured private Worker"); + requireThat(knowledge.workers_dev === false && knowledge.preview_urls === false, + "Knowledge Worker must remain private"); + requireThat(main.containers?.some(item => item.class_name === "MultiLLMProxyContainer" && item.image === "./Dockerfile") + && main.durable_objects?.bindings?.some(item => item.name === "MULTILLM_PROXY_CONTAINER" + && item.class_name === "MultiLLMProxyContainer"), "Main Worker needs its Container binding and image"); + return first.database_name; +} + +export function checkPrerequisites({ main, knowledge, run, migrations = localMigrations() }) { + const database = checkConfig(main, knowledge); + const applied = remoteAppliedMigrations(database, run); + requireThat(migrations.every(name => applied.includes(name)) && applied.every(name => migrations.includes(name)), + "D1 migration ledger must exactly match this release"); + const results = JSON.parse(run(["d1", "execute", database, "--remote", "--json", "--command", "PRAGMA table_info(control_users)"])); + const columns = results.flatMap(item => item.results ?? []).map(item => item.name); + requireThat(REQUIRED_COLUMNS.every(name => columns.includes(name)), "Account policy columns are missing from D1"); + const secrets = JSON.parse(run(["secret", "list", "--config", "wrangler.jsonc"])); + requireThat(REQUIRED_SECRETS.every(name => main.vars?.[name] || secrets.some(item => item.name === name)), + "Main Worker needs ADMIN_API_KEY, FLASK_SECRET_KEY and JWT_SECRET bindings"); +} + +export function checkRelease(options) { + checkPrerequisites(options); + const { commit, run } = options; + requireThat(/^[0-9a-f]{40}$/.test(commit), "Release needs a full commit identity"); + const deployment = JSON.parse(run(["deployments", "status", "--json", "--config", "wrangler.knowledge.jsonc"])); + requireThat(deployment.versions?.length === 1 && deployment.versions[0].percentage === 100, + "Private Worker must have one fully active version"); + const version = JSON.parse(run(["versions", "view", deployment.versions[0].version_id, "--json", "--config", "wrangler.knowledge.jsonc"])); + requireThat(version.annotations?.["workers/tag"] === commit, "Deploy the private Worker from this commit before the main Worker"); +} + +function main(args) { + const mainConfig = JSON.parse(readFileSync(new URL("wrangler.jsonc", ROOT))); + const knowledge = JSON.parse(readFileSync(new URL("wrangler.knowledge.jsonc", ROOT))); + try { + checkConfig(mainConfig, knowledge); + if (args[0] === "--check" || process.env.WRANGLER_COMMAND === "dev") { + console.log("Release configuration checked (no deployment admission)."); + return 0; + } + requireThat(["--prepare", "--deploy"].includes(args[0]), "Use --check, --prepare or --deploy"); + const git = args => execFileSync("git", args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); + const commit = git(["rev-parse", "HEAD"]); + requireThat(!git(["status", "--porcelain", "--untracked-files=no", "--", ".", ":(exclude)worker/build-id.mjs"]), + "Deploy from a clean committed checkout"); + const run = args => execFileSync("npx", ["--no-install", "wrangler", ...args], { + cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60000, + env: { ...process.env, WRANGLER_SEND_METRICS: "false" }, + }); + const options = { main: mainConfig, knowledge, commit, run }; + if (args[0] === "--prepare") { + checkPrerequisites(options); + console.log("Release prerequisites checked before private Worker deployment."); + return 0; + } + checkRelease(options); + console.log("Release admitted: migrations, account schema, required bindings and private Worker match."); + return 0; + } catch (error) { + // CLI output may contain environment details; print only our own diagnostics. + const message = error instanceof PreflightError ? error.message : "Could not verify release prerequisites"; + console.error(`Deployment preflight refused: ${message}. No main upload is admitted.`); + return 1; + } +} +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = main(process.argv.slice(2)); diff --git a/services/accounted_dispatch.py b/services/accounted_dispatch.py index dba61099..1043a305 100644 --- a/services/accounted_dispatch.py +++ b/services/accounted_dispatch.py @@ -29,6 +29,10 @@ def release_outer_accounting() -> None: def accounted_dispatch(payload: dict, dispatch, *, kind: str, skip_rate: bool = False): user = getattr(g, "authenticated_user", None) or {} model = payload["model"] + from services.key_data_guards import model_permitted + if not model_permitted(user, model): + raise APIError("This API key cannot use this provider data class", 403, + {"error": "provider_class_not_allowed"}) if not key_controls.model_allowed(user, model): raise APIError("This API key is not allowed to use the subrequest model", 403, payload={"error": "model_not_allowed"}) diff --git a/services/auth_service.py b/services/auth_service.py index 81ca3b91..d1ddb143 100644 --- a/services/auth_service.py +++ b/services/auth_service.py @@ -153,6 +153,8 @@ def _ensure_users_schema(cls, connection: sqlite3.Connection) -> None: "expires_at": "TEXT", "secret_scan_mode": "TEXT", # nosec B105 - a column type, not a password "shadow_eval_rate": "DOUBLE PRECISION", + "official_only": "INTEGER", + "forced_provider_preferences": "TEXT", } for column_name, column_definition in required_columns.items(): if column_name not in columns: @@ -193,7 +195,9 @@ def _create_users_table(cls, connection: sqlite3.Connection, table_name: str = " allowed_ips TEXT, expires_at TEXT, secret_scan_mode TEXT, - shadow_eval_rate DOUBLE PRECISION + shadow_eval_rate DOUBLE PRECISION, + official_only INTEGER, + forced_provider_preferences TEXT ) """ ) @@ -279,7 +283,9 @@ def _backfill_user_metadata(cls, connection: sqlite3.Connection) -> None: @classmethod def _row_to_user(cls, row: sqlite3.Row) -> Dict[str, Any]: + from services.dispatch_policy import envelope return { + "data_policy": envelope(row), "username": row["username"], "api_key_hash": row["api_key_hash"], "api_key_prefix": row["api_key_prefix"], @@ -311,7 +317,7 @@ def _reload_user_cache(cls) -> None: username, api_key_hash, api_key_prefix, scopes, is_admin, created_at, last_login, last_used_at, last_used_ip, created_by, rotated_at, revoked_at, daily_budget_usd, - monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate + monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate, official_only, forced_provider_preferences FROM users ORDER BY username """) @@ -336,7 +342,7 @@ def _load_user_by_username(cls, username: str) -> Optional[Dict[str, Any]]: username, api_key_hash, api_key_prefix, scopes, is_admin, created_at, last_login, last_used_at, last_used_ip, created_by, rotated_at, revoked_at, daily_budget_usd, - monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate + monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate, official_only, forced_provider_preferences FROM users WHERE username = ? """), @@ -366,7 +372,7 @@ def _load_users_by_api_key_prefix(cls, api_key_prefix: str) -> List[tuple[str, D username, api_key_hash, api_key_prefix, scopes, is_admin, created_at, last_login, last_used_at, last_used_ip, created_by, rotated_at, revoked_at, daily_budget_usd, - monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate + monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate, official_only, forced_provider_preferences FROM users WHERE api_key_prefix = ? AND revoked_at IS NULL ORDER BY username @@ -438,9 +444,9 @@ def _persist_user( username, api_key_hash, api_key_prefix, scopes, is_admin, created_at, last_login, last_used_at, last_used_ip, created_by, rotated_at, revoked_at, daily_budget_usd, - monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate + monthly_budget_usd, allowed_models, allowed_ips, expires_at, secret_scan_mode, shadow_eval_rate, official_only, forced_provider_preferences ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(username) DO UPDATE SET api_key_hash = excluded.api_key_hash, api_key_prefix = excluded.api_key_prefix, @@ -459,7 +465,9 @@ def _persist_user( allowed_ips = excluded.allowed_ips, expires_at = excluded.expires_at, secret_scan_mode = excluded.secret_scan_mode, - shadow_eval_rate = excluded.shadow_eval_rate + shadow_eval_rate = excluded.shadow_eval_rate, + official_only = excluded.official_only, + forced_provider_preferences = excluded.forced_provider_preferences """, ( username, @@ -915,6 +923,7 @@ def _public_user(cls, username: str, user: Dict[str, Any]) -> Dict[str, Any]: "rotated_at": serialize_datetime(user["rotated_at"]), "revoked_at": serialize_datetime(user["revoked_at"]), **key_controls.public(user), + "data_policy": user.get("data_policy"), } @staticmethod diff --git a/services/cloudflare_ai.py b/services/cloudflare_ai.py index 4079a23b..4cf18a30 100644 --- a/services/cloudflare_ai.py +++ b/services/cloudflare_ai.py @@ -42,7 +42,9 @@ def post(path: str, payload: dict, timeout) -> requests.Response | Response: with requests.Session() as session: session.trust_env = False try: - response = session.post(ORIGIN + path, json=payload, timeout=timeout, allow_redirects=False, stream=True) + from services.provider_dispatch import provider_http + response = provider_http("cloudflare", "POST", ORIGIN + path, transport=session.request, + binding=True, json=payload, timeout=timeout, stream=True) except requests.exceptions.ConnectionError as error: connect = isinstance(error, requests.exceptions.ConnectTimeout) or "NewConnectionError" in repr(error) return _transport_failure("connect" if connect else "interrupted") diff --git a/services/control_plane_backup.py b/services/control_plane_backup.py index 04bc1971..59848ca3 100644 --- a/services/control_plane_backup.py +++ b/services/control_plane_backup.py @@ -27,7 +27,7 @@ } ADDITIVE_TABLES = {"intelligence_policy", "intelligence_reservations"} # Columns added later; older backups omit them and restore them as NULL. -OPTIONAL_FIELDS = {"users": ("daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate")} +OPTIONAL_FIELDS = {"users": ("daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate", "official_only", "forced_provider_preferences")} STORES = { "auth": ("AUTH_DB_PATH", "auth.sqlite3"), "models": ("MODEL_REGISTRY_DB_PATH", "model_registry.sqlite3"), diff --git a/services/dispatch_network.py b/services/dispatch_network.py new file mode 100644 index 00000000..ef5c4c1d --- /dev/null +++ b/services/dispatch_network.py @@ -0,0 +1,55 @@ +"""A short-lived network capability issued only after dispatch admission.""" +from contextlib import contextmanager +from contextvars import ContextVar + +_active = ContextVar("admitted_provider_network", default=False) + + +def network_admitted(address=None): + scope = _active.get() + if not scope or not scope["active"]: + return False + destination = scope.get("destination") + if destination is None: + return True + if not isinstance(address, tuple) or address[1] != destination[1]: + return False + if address[:2] == destination: + return True + # Resolve only when a real connection is attempted; mocked transports and + # disabled exporters must retain their existing no-network behavior. + import socket + try: + addresses = scope.get("resolved") + if addresses is None: + addresses = {item[4][:2] for item in socket.getaddrinfo(*destination, type=socket.SOCK_STREAM)} + scope["resolved"] = addresses + return address[:2] in addresses + except OSError: + return False + + +@contextmanager +def admitted_network(): + scope = {"active": True} + token = _active.set(scope) + try: + yield + finally: + scope["active"] = False + _active.reset(token) + + +@contextmanager +def control_network(url): + """Explicit non-provider control traffic, confined to its configured endpoint.""" + from urllib.parse import urlsplit + target = urlsplit(url) + port = target.port or (443 if target.scheme == "https" else 80) + scope = {"active": True, "destination": (target.hostname, port)} + token = _active.set(scope) + try: + yield + finally: + scope["active"] = False + _active.reset(token) diff --git a/services/dispatch_policy.py b/services/dispatch_policy.py new file mode 100644 index 00000000..d9a5bed1 --- /dev/null +++ b/services/dispatch_policy.py @@ -0,0 +1,23 @@ +"""Versioned, explicit data policy carried by verified account records.""" + +from error_handlers import APIError +from services.key_data_guards import stored_controls + +VERSION = 1 +FIELDS = frozenset({"version", "official_only", "forced_provider_preferences"}) + + +def unavailable(): + return APIError("Current key data controls could not be verified", 503, + {"error": "key_data_controls_unavailable"}) + + +def envelope(row): + controls = stored_controls(row) + return {"version": VERSION, **controls} + + +def verify(value): + if not isinstance(value, dict) or set(value) != FIELDS or type(value.get("version")) is not int or value["version"] != VERSION: + raise unavailable() + return envelope(value) diff --git a/services/gemini_credentials.py b/services/gemini_credentials.py new file mode 100644 index 00000000..4cbcf115 --- /dev/null +++ b/services/gemini_credentials.py @@ -0,0 +1,148 @@ +"""Credential selection for native and translated Gemini requests.""" + +import logging +import os +import re +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit + +from flask import g, has_request_context, request + +from error_handlers import APIError +from services.auth_primitives import usable_credential + +logger = logging.getLogger(__name__) + + +def restricted(provider): + return provider == "gemini" and has_request_context() and bool( + (getattr(g, "authenticated_user", None) or {}).get("official_only")) + + +def _refuse_override(): + raise APIError("Caller provider credentials are not allowed for this key", 403, + {"error": "provider_credential_not_allowed"}) + + +def reject_caller_credentials(provider): + if not restricted(provider): + return + if any(name.lower() == "key" for name in request.args) or "x-goog-api-key" in request.headers: + _refuse_override() + gateway = request.headers.get("x-multillm-api-key") + authorization = request.headers.get("authorization") + if gateway and authorization: + bearer = re.fullmatch(r"Bearer\s+(.+)", authorization.strip(), re.IGNORECASE) + if not bearer or bearer[1].strip() != gateway.strip(): + _refuse_override() + + +def prepare_credentials(url, headers, params, provider, auth_token, auth): + headers, params = dict(headers or {}), dict(params or {}) + if restricted(provider): + reject_caller_credentials(provider) + if any(name.lower() == "key" for name in params) or any( + name.lower() == "key" for name, _ in parse_qsl(urlsplit(url).query, keep_blank_values=True)): + _refuse_override() + # Only this operator binding is covered by the paid-tier attestation. Do not + # recover through a legacy alias, cached credential or caller-supplied key. + key = usable_credential(os.environ.get("GEMINI_API_KEY"), "GEMINI_API_KEY") + if not key: + raise APIError("Attested provider credential is unavailable", 503, + {"error": "attested_credential_unavailable"}) + gateway_authorization = request.headers.get("authorization") + for name, value in headers.items(): + if name.lower() == "x-goog-api-key" and value != key: + _refuse_override() + if name.lower() == "authorization" and value not in {f"Bearer {key}", gateway_authorization}: + _refuse_override() + else: + url, params, key = _legacy_credentials(url, params, provider, auth_token, auth) + headers = {name: value for name, value in headers.items() + if name.lower() not in {"authorization", "x-goog-api-key"}} + headers["x-goog-api-key"] = key + return url, headers, params + + +def _legacy_credentials(url, params, api_provider, auth_token, auth): + # Extract API key from URL parameters and rebuild the URL without it + api_key = None + + # Check if key is in params + if params and 'key' in params: + api_key = params.pop('key') + if isinstance(api_key, (list, tuple)): + api_key = api_key[0] if api_key else None + logger.info(f"Found API key in URL parameters for {api_provider}") + + # If no key in params, check if it's in the URL + else: + parsed_url = urlsplit(url) + query_pairs = parse_qsl(parsed_url.query, keep_blank_values=True) + filtered_query_pairs = [] + for key, value in query_pairs: + if key == 'key' and api_key is None: + api_key = value + logger.info(f"Found API key in URL for {api_provider}") + else: + filtered_query_pairs.append((key, value)) + + if api_key is not None: + url = urlunsplit( + ( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + urlencode(filtered_query_pairs), + parsed_url.fragment, + ) + ) + + # If still no API key, use the auth token passed from make_request + # But first check if it's the admin API key - if so, use the Gemini key from env instead + if not api_key and auth_token: + admin_api_key = os.environ.get('ADMIN_API_KEY') + if auth_token == admin_api_key: + # Using admin key, look up the actual provider key instead + logger.info(f"Admin API key detected - using {api_provider} API key from env instead") + api_key = auth.get_api_key(api_provider) + if not api_key: + # Try alternative keys + if api_provider == 'gemma': + api_key = auth.get_api_key('gemini') + elif api_provider == 'gemini': + api_key = auth.get_api_key('gemma') + else: + # Not the admin key, might be a direct Gemini key + api_key = auth_token + logger.info(f"Using API key from Authorization header for {api_provider}") + + # If still no API key, get from auth service + if not api_key: + # First try with the exact api_provider + api_key = auth.get_api_key(api_provider) + + # If using 'gemma' as provider but no gemma key found, try with 'gemini' as they share the same key + if not api_key and api_provider == 'gemma': + api_key = auth.get_api_key('gemini') + logger.info("Using Gemini API key for Gemma model") + + # If using 'gemini' as provider but no gemini key found, try with 'gemma' as a fallback + elif not api_key and api_provider == 'gemini': + api_key = auth.get_api_key('gemma') + logger.info("Using Gemma API key for Gemini model") + + if not api_key: + raise APIError(f"No API key found for {api_provider}. Please set {api_provider.upper()}_API_KEY in your .env file.", status_code=401) + + logger.info(f"Using API key from AuthService for {api_provider}") + + # Validate API key format - Gemini keys should start with "AIza" + if not api_key.startswith("AIza"): + logger.warning(f"API key for {api_provider} doesn't match expected format (should start with 'AIza')") + # Try to find a valid key from the environment + env_key = os.environ.get(f'{api_provider.upper()}_API_KEY') + if env_key and env_key.startswith("AIza"): + logger.info(f"Found valid {api_provider} API key format in environment, using it instead") + api_key = env_key + + return url, params, api_key diff --git a/services/intelligence_transport.py b/services/intelligence_transport.py index f77e0ab2..84083d39 100644 --- a/services/intelligence_transport.py +++ b/services/intelligence_transport.py @@ -72,7 +72,8 @@ def __init__( self.response = None self._response_lock = threading.Lock() self.max_bytes = max_bytes - self.send, self.record = send, record + from services.provider_dispatch import capture_dispatch + self.send, self.record = capture_dispatch(send), record self.record_response = record_response if not _TRANSPORT_SLOTS.acquire(blocking=False): raise GatewayError( @@ -395,7 +396,12 @@ def start( if content_type: headers["Content-Type"] = content_type - dispatch_data = protect_body(upstream.data if data is None else data, headers, provider=provider) + from services.key_data_guards import guard_request + guarded_data = guard_request(provider, upstream.data if data is None else data) + from services.gemini_credentials import prepare_credentials, restricted + if restricted(provider): + url, headers, _ = prepare_credentials(url, headers, {}, provider, token, self.auth) + dispatch_data = protect_body(guarded_data, headers, provider=provider) scope = selection_context(provider, model, config=self.config) from services.generation_deadline import bounded_timeout, current_deadline diff --git a/services/key_controls.py b/services/key_controls.py index 017ee8b0..921f9127 100644 --- a/services/key_controls.py +++ b/services/key_controls.py @@ -18,10 +18,10 @@ from werkzeug.security import check_password_hash from error_handlers import APIError -from services import user_store, stream_cost_breaker +from services import user_store, stream_cost_breaker, key_data_guards from services.auth_primitives import build_api_key_prefix, normalized_username -CONTROL_FIELDS = ("daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate") +CONTROL_FIELDS = ("daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate", "official_only", "forced_provider_preferences") MAX_BUDGET_USD = 1_000_000_000 MAX_PATTERNS = 64 MAX_RANGES = 64 @@ -148,6 +148,7 @@ def validate(payload: Mapping[str, Any]) -> dict[str, Any]: "secret_scan_mode": mode, "shadow_eval_rate": rate, **stream_cost_breaker.validated_control(payload), + **key_data_guards.validate_controls(payload), } @@ -164,6 +165,7 @@ def from_storage(row: Mapping[str, Any]) -> dict[str, Any]: rate = row.get("shadow_eval_rate") if hasattr(row, "get") else row_value(row, "shadow_eval_rate") if type(rate) in (int, float) and 0 <= rate <= 0.2: controls["shadow_eval_rate"] = float(rate) + controls.update(key_data_guards.stored_controls(row)) controls.update(stream_cost_breaker.storage_control(row)) return controls @@ -187,6 +189,8 @@ def public(controls: Mapping[str, Any]) -> dict[str, Any]: "secret_scan_mode": controls.get("secret_scan_mode"), "shadow_eval_rate": controls.get("shadow_eval_rate"), **stream_cost_breaker.public_control(controls), + "official_only": bool(controls.get("official_only")), + "forced_provider_preferences": key_data_guards.preferences(controls.get("forced_provider_preferences")), } @@ -210,6 +214,8 @@ def _glob(pattern: str) -> re.Pattern: def model_allowed(user: Mapping[str, Any], model: Any) -> bool: """Whether a model ID matches the key's allowlist; ``*`` matches any characters.""" + if not key_data_guards.model_permitted(user, model): + return False patterns = model_patterns(user) if patterns is None: allowed = True @@ -224,6 +230,8 @@ def model_allowed(user: Mapping[str, Any], model: Any) -> bool: def provider_allowed(user: Mapping[str, Any], provider: str) -> bool: """Whether a request that names no model may reach a provider namespace.""" + if not key_data_guards.model_permitted(user, provider + ":*"): + return False patterns = model_patterns(user) if patterns is None: allowed = True @@ -293,9 +301,11 @@ def client_ip(request) -> Optional[str]: def _security_user(auth, row): """Reject malformed authority records before permissive legacy decoding.""" + if not isinstance(row, dict) or not set(user_store.USER_FIELDS).issubset(row): + raise ValueError("Incomplete security account") + row = stream_cost_breaker.d1_row(row) if not isinstance(row, dict) or set(row) != set(user_store.USER_FIELDS) | ({stream_cost_breaker.CAP_FIELD} if stream_cost_breaker.enabled() else set()): raise ValueError("Incomplete security account") - stream_cost_breaker.d1_row(row) if normalized_username(row["username"]) != row["username"]: raise ValueError("Invalid security account identity") if not row["api_key_hash"] or not row["api_key_prefix"] or not re.fullmatch(r"[A-Za-z0-9:_.,-]{0,512}", row["scopes"]): @@ -308,7 +318,11 @@ def _security_user(auth, row): models = row["allowed_models"] if models is not None and (not models or not all(MODEL_PATTERN.fullmatch(item) for item in models.split(","))): raise ValueError("Invalid security model grants") - controls = validate({name: row[name] for name in CONTROL_FIELDS}) + payload = {name: row[name] for name in CONTROL_FIELDS} + if payload["official_only"] is not None: + payload["official_only"] = bool(payload["official_only"]) + payload["forced_provider_preferences"] = key_data_guards.preferences(payload["forced_provider_preferences"]) + controls = validate(payload) if any(row[name] is not None and controls[name] is None for name in CONTROL_FIELDS): raise ValueError("Invalid security account controls") return auth._row_to_user(row) diff --git a/services/key_data_guards.py b/services/key_data_guards.py new file mode 100644 index 00000000..92aaa2a7 --- /dev/null +++ b/services/key_data_guards.py @@ -0,0 +1,126 @@ +"""Provider eligibility and administrator-owned request preferences.""" + +import json +from copy import deepcopy +from pathlib import Path + +from flask import current_app, g, has_app_context, has_request_context + +from error_handlers import APIError + +DATA_CLASSES = json.loads((Path(__file__).parent.parent / "providers/data-classes.json").read_text()) +MAX_PREFERENCES_BYTES = 4096 + + +def preferences(value): + """Decode storage or API values, rejecting malformed policy rather than dropping it.""" + if value is None: + return None + try: + parsed = json.loads(value) if isinstance(value, str) else value + encoded = json.dumps(parsed, allow_nan=False) + if not isinstance(parsed, dict) or len(encoded.encode()) > MAX_PREFERENCES_BYTES: + raise ValueError() + if parsed.get("data_collection", "deny") not in {"allow", "deny"}: + raise ValueError() + return parsed + except (ValueError, TypeError, RecursionError): + raise APIError("forced_provider_preferences must be a bounded JSON object", 400) from None + + +def validate_controls(payload): + official = payload.get("official_only") + if official is not None and type(official) is not bool: + raise APIError("official_only must be a boolean", 400) + raw = payload.get("forced_provider_preferences") + if raw is not None and not isinstance(raw, dict): + raise APIError("forced_provider_preferences must be a JSON object", 400) + forced = preferences(raw) + return {"official_only": int(official) if official is not None else None, + "forced_provider_preferences": json.dumps(forced, separators=(",", ":")) if forced is not None else None} + + +def stored_controls(row): + def value(name): + try: + return row[name] + except (KeyError, IndexError, TypeError): + raise APIError("Stored key data controls are incomplete", 503) from None + official = value("official_only") + if official is not None and (type(official) not in (bool, int) or official not in (0, 1)): + raise APIError("Stored key data controls are invalid", 503) + raw = value("forced_provider_preferences") + try: + forced = preferences(raw) + except APIError: + raise APIError("Stored key data controls are invalid", 503) from None + return {"official_only": official, + "forced_provider_preferences": json.dumps(forced, separators=(",", ":")) if forced is not None else None} + + +def provider_data_class(provider, config=None): + config = config if config is not None else (current_app.config if has_app_context() else {}) + provider = str(provider).strip().lower() + free = str(config.get("FREE_ROUTE_FREE_TIER_PROVIDERS", "")).replace(",", " ").split() + if provider in free or provider == "free": + return "free" + if provider == "gemini": + return "official" if config.get("GEMINI_DATA_TIER", "free") == "paid" else "free" + return DATA_CLASSES.get(provider, "subscription" if provider.startswith("ce-") else "reseller") + + +def has_data_controls(user): + """Even an empty forced object represents an administrator-owned data policy.""" + return bool(user.get("official_only")) or user.get("forced_provider_preferences") is not None + + +def model_permitted(user, model, config=None): + if not user.get("official_only"): + return True + provider = str(model or "").split(":", 1)[0].lower() + if provider in {"auto", "cascade", "intelligence"}: + return True # Each concrete candidate is checked before dispatch. + return provider_data_class(provider, config) == "official" + + +def eligible_models(models, user): + """Filter route candidates and refuse before dispatch if none remain.""" + eligible = [model for model in models if model_permitted(user, model)] + if not eligible: + raise APIError("No eligible provider data class remains", 403, {"error": "provider_class_not_allowed"}) + return eligible + + +def require_provider(provider, user=None): + user = user if user is not None else (getattr(g, "authenticated_user", None) or {} if has_request_context() else {}) + if not model_permitted(user, provider + ":*"): + raise APIError("This API key cannot use this provider data class", 403, + {"error": "provider_class_not_allowed"}) + from services.gemini_credentials import reject_caller_credentials + reject_caller_credentials(provider) + + +def merge_preferences(caller, forced): + result = deepcopy(caller) if isinstance(caller, dict) else {} + for name, value in forced.items(): + if name == "data_collection" and result.get(name) == "deny": + continue + result[name] = merge_preferences(result.get(name), value) if isinstance(value, dict) else deepcopy(value) + return result + + +def guard_request(provider, data): + """Final transport boundary covers raw, unified, routed and internal requests.""" + require_provider(provider) + user = getattr(g, "authenticated_user", None) or {} if has_request_context() else {} + forced = preferences(user.get("forced_provider_preferences")) + if provider != "openrouter" or not forced or data is None: + return data + try: + payload = json.loads(data) + if not isinstance(payload, dict): + raise ValueError() + except (ValueError, TypeError): + raise APIError("A JSON object is required for enforced provider preferences", 400) from None + payload["provider"] = merge_preferences(payload.get("provider"), forced) + return json.dumps(payload).encode() diff --git a/services/knowledge_client.py b/services/knowledge_client.py index 8b6586a2..1b8b53b3 100644 --- a/services/knowledge_client.py +++ b/services/knowledge_client.py @@ -132,7 +132,8 @@ def _submit(body, stopped, deadline, results): session.mount("http://", HTTPAdapter(max_retries=0)) if stopped.is_set(): return - with session.post(ENDPOINT, data=body, + from services.provider_dispatch import provider_http + with provider_http("knowledge", "POST", ENDPOINT, transport=session.request, binding=True, data=body, headers={"Content-Type": "application/json", "Accept": "application/json", "Accept-Encoding": "identity"}, timeout=(3, DEADLINE_SECONDS), allow_redirects=False, stream=True) as response: @@ -173,7 +174,9 @@ def dispatch(operation, user, payload=None): stopped = threading.Event() results = queue.Queue(maxsize=1) deadline = time.monotonic() + DEADLINE_SECONDS - thread = threading.Thread(target=_submit, args=(body, stopped, deadline, results), + from services.provider_dispatch import capture_dispatch + thread = threading.Thread(target=capture_dispatch(_submit, reference={"username": user.get("username") or user.get("id"), + "api_key_prefix": user.get("api_key_prefix")}), args=(body, stopped, deadline, results), daemon=True, name="knowledge-request") try: thread.start() diff --git a/services/media_storage.py b/services/media_storage.py index abd4ab1a..ee75717a 100644 --- a/services/media_storage.py +++ b/services/media_storage.py @@ -26,6 +26,7 @@ from error_handlers import APIError from services.media_signing import FILE_ID, file_link_params from services.media_urls import public_https_url +from services.provider_dispatch import provider_http logger = logging.getLogger(__name__) @@ -78,7 +79,7 @@ def fetch_public(url: str, *, max_bytes: int = MAX_FETCH_BYTES, field: str = "im url = public_https_url(url, field) with _session() as session: try: - with session.post(f"{ORIGIN}/v1/fetch", json={"url": url, "max_bytes": max_bytes}, timeout=FETCH_TIMEOUT, + with provider_http("media", "POST", f"{ORIGIN}/v1/fetch", transport=session.request, binding=True, json={"url": url, "max_bytes": max_bytes}, timeout=FETCH_TIMEOUT, allow_redirects=False, stream=True) as response: if response.status_code != 200: code = _error_code(response) @@ -168,7 +169,7 @@ def import_url(file_id: str, url: str, *, owner: str, kind: str, model: str | No """The Worker downloads a provider URL straight into R2.""" try: with _session() as session: - response = session.post(_file_url(file_id, "/import"), timeout=STORE_TIMEOUT, allow_redirects=False, + response = provider_http("media", "POST", _file_url(file_id, "/import"), transport=session.request, binding=True, timeout=STORE_TIMEOUT, allow_redirects=False, json={"url": url, "metadata": {"owner": owner, "kind": kind, **({"model": model} if model else {})}}) return _stored(response) @@ -365,7 +366,10 @@ def video_file_id(job_id: str) -> str: def store_video(file_id: str, url: str, headers: dict, *, owner: str, model: str | None) -> dict | None: """Copy a finished video into R2 once; None when its size is unknown or too large.""" try: - upstream = requests.get(url, headers=headers, timeout=VIDEO_TIMEOUT, stream=True, allow_redirects=True) + from services.provider_assets import asset_http + provider = str(model or "unknown").split(":", 1)[0] + upstream = asset_http(provider, url, headers=headers, timeout=VIDEO_TIMEOUT, stream=True, + reference={"username": owner}) except requests.exceptions.RequestException: return None with upstream: diff --git a/services/native_token_count.py b/services/native_token_count.py index 7ae2dcb7..fb0b38be 100644 --- a/services/native_token_count.py +++ b/services/native_token_count.py @@ -130,7 +130,8 @@ def count_native_tokens( session = ProxyService._get_provider_session(provider, raw_passthrough=True) response = None try: - response = session.post(url=url, headers=headers, json=body, stream=True, + from services.provider_dispatch import provider_http + response = provider_http(provider, "POST", url, transport=session.request, headers=headers, json=body, stream=True, allow_redirects=False, timeout=(5, 10)) status = response.status_code if status >= 400: diff --git a/services/prompt_cache_cost.py b/services/prompt_cache_cost.py index ab78ea62..7fd9b7ec 100644 --- a/services/prompt_cache_cost.py +++ b/services/prompt_cache_cost.py @@ -89,12 +89,12 @@ def from_body(cls, value: Any) -> CacheObservation | None: key in usage for key in ("cache_read_input_tokens", "cache_creation_input_tokens")) details = usage.get("prompt_tokens_details", usage.get("input_tokens_details")) details = details if isinstance(details, dict) else {} - raw = count(typed.input_tokens) + raw = count(usage.get("input_tokens")) if anthropic else count(typed.input_tokens) read = count(usage.get("cache_read_input_tokens")) if anthropic else count(details.get("cached_tokens")) write = count(usage.get("cache_creation_input_tokens")) if anthropic else count(details.get("cache_write_tokens", 0 if raw is not None or details else None)) source = "anthropic" if anthropic else "openai" if raw is not None or details else "unknown" total = _sum((raw, read, write)) if anthropic else raw - return cls(total, count(typed.output_tokens), typed.basis, typed.provenance, raw, read, write, source) + return cls(total, count(typed.output_tokens), typed.basis, typed.provenance, raw, read, write, source, cached_input_tokens=typed.cached_input_tokens, reasoning_tokens=typed.reasoning_tokens) def merge(self, later: UsageObservation) -> CacheObservation: if not isinstance(later, CacheObservation): @@ -110,7 +110,8 @@ def latest(old, new): output = latest(self.output_tokens, later.output_tokens) known = any(v is not None for v in (raw, read, write, output)) return CacheObservation(total, output, "provider" if known else "unknown", ("provider",) if known else (), - raw, read, write, source) + raw, read, write, source, cached_input_tokens=read, + reasoning_tokens=later.reasoning_tokens if later.reasoning_tokens is not None else self.reasoning_tokens) def buckets_from(usage: UsageObservation | None) -> dict: diff --git a/services/provider_assets.py b/services/provider_assets.py new file mode 100644 index 00000000..da5836b2 --- /dev/null +++ b/services/provider_assets.py @@ -0,0 +1,37 @@ +"""Bounded asset downloads, with fresh policy admission at every redirect.""" +from urllib.parse import urljoin, urlsplit + +from error_handlers import APIError +from services.provider_credentials import api_origin, origin, strip_credentials +from services.provider_dispatch import provider_http + +MAX_REDIRECTS = 5 + + +def asset_http(provider, url, *, headers=None, reference=None, **options): + # Only documented authenticated content APIs may start with credentials. + # Status-returned xAI and private-binding assets are always public downloads. + authenticated = provider in {"openai", "gemini"} and api_origin(provider, url) + headers = dict(headers or {}) + for hop in range(MAX_REDIRECTS + 1): + parsed = urlsplit(url) + if origin(url) is None or parsed.scheme != "https" or parsed.port not in (None, 443): + raise APIError("The provider returned an unsupported video location", 502) + if not authenticated: + headers = strip_credentials(headers) + response = provider_http(provider, "GET", url, headers=headers, reference=reference, + public_asset=not authenticated, **options) + if not 300 <= response.status_code < 400: + return response + location = response.headers.get("Location") + response.close() + if not location or hop == MAX_REDIRECTS: + raise APIError("The video download redirect could not be resolved", 502) + destination = urljoin(url, location) + # Once an API redirects to an asset origin, never reintroduce its secrets, + # including if a subsequent redirect returns to the API origin. + if origin(destination) != origin(url): + authenticated = False + headers = strip_credentials(headers) + url = destination + raise AssertionError("Unreachable redirect limit") diff --git a/services/provider_credentials.py b/services/provider_credentials.py new file mode 100644 index 00000000..cd1db5c4 --- /dev/null +++ b/services/provider_credentials.py @@ -0,0 +1,100 @@ +"""Origin-bound operator credentials; public assets never receive API secrets.""" +import json +import os +import shutil +import subprocess +from pathlib import Path +from urllib.parse import urlsplit + +from flask import current_app, has_app_context + +from error_handlers import APIError +from services.auth_primitives import provider_api_key_env_names, usable_credential + +SECRET_HEADERS = {"authorization", "proxy-authorization", "x-api-key", "x-goog-api-key", "api-key", "cookie"} + + +def origin(url): + try: + parsed = urlsplit(url) + if parsed.username or parsed.password or not parsed.hostname: + return None + return parsed.scheme.lower(), parsed.hostname.lower(), parsed.port or (443 if parsed.scheme == "https" else 80) + except ValueError: + return None + + +def api_origin(provider, url): + from config import Config + bases = current_app.config.get("API_BASE_URLS", Config.API_BASE_URLS) if has_app_context() else Config.API_BASE_URLS + allowed = {origin(bases.get(provider, ""))} + # Native Gemini and Vertex protocols use the same operator API binding. + if provider in {"gemini", "gemma", "palm"}: + allowed.add(origin("https://generativelanguage.googleapis.com")) + allowed.discard(None) + target = origin(url) + return target is not None and target[0] == "https" and target in allowed + + +def strip_credentials(headers): + return {name: value for name, value in headers.items() if name.lower() not in SECRET_HEADERS} + + +def unavailable_credential(): + return APIError("Attested provider credential is unavailable", 503, {"error": "attested_credential_unavailable"}) + + +def _google_binding(): + raw = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS_JSON", "").strip() + path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "").strip() + if raw: + return "json", raw + if path: + return "file", Path(path).read_text() + return None + + +def google_operator_token(): + """Refresh the current binding, never the legacy process-wide token cache.""" + try: + binding = _google_binding() + if binding: + from google.oauth2 import service_account + from google.auth.transport.requests import Request + info = json.loads(binding[1]) + if origin(info.get("token_uri", "")) != origin("https://oauth2.googleapis.com"): + raise ValueError() + credentials = service_account.Credentials.from_service_account_info(info, + scopes=["https://www.googleapis.com/auth/cloud-platform"]) + credentials.refresh(Request()) + token = credentials.token + if binding != _google_binding(): + raise ValueError() + else: + binary = shutil.which("gcloud") + if not binary: + raise ValueError() + def run(*args): + return subprocess.run([binary, *args, "--quiet"], capture_output=True, text=True, + check=True, timeout=30).stdout.strip() + account = run("config", "get-value", "account") + if not account or account == "(unset)": + raise ValueError() + token = run("auth", "print-access-token", account) + if _google_binding() is not None or account != run("config", "get-value", "account"): + raise ValueError() + if not isinstance(token, str) or not token.strip(): + raise ValueError() + return token.strip() + except Exception: + raise unavailable_credential() from None + + +def operator_credential(provider): + if provider == "googleai": + return google_operator_token() + names = ("GEMINI_API_KEY",) if provider == "gemini" else provider_api_key_env_names(provider) + key = next((usable_credential(os.environ.get(name), name) for name in names if usable_credential(os.environ.get(name))), None) + if not key: + raise unavailable_credential() + return key diff --git a/services/provider_dispatch.py b/services/provider_dispatch.py new file mode 100644 index 00000000..1c528663 --- /dev/null +++ b/services/provider_dispatch.py @@ -0,0 +1,145 @@ +"""The sole provider network boundary, including deferred and non-chat work. + +Only an account reference crosses a thread boundary. Its policy is reread at each +submission; a cached principal, environment administrator or old RPC cannot grant +generation authority. Private service bindings receive the same verified reference. +""" + +import json +from contextvars import ContextVar +from contextlib import contextmanager +from urllib.parse import parse_qsl, urlsplit + +import requests +from flask import current_app, g, has_app_context, has_request_context, request + +from error_handlers import APIError +from services.provider_credentials import api_origin, operator_credential, strip_credentials, unavailable_credential +from services.dispatch_network import admitted_network +from services.dispatch_policy import unavailable, verify +from services.key_data_guards import has_data_controls, merge_preferences, model_permitted, preferences + +_reference = ContextVar("provider_principal", default=None) + + +def principal_reference(): + if _reference.get() is not None: + return _reference.get() + user = getattr(g, "authenticated_user", None) if has_request_context() else None + if user: + return {"username": user.get("username") or user.get("id"), "api_key_prefix": user.get("api_key_prefix")} + return _reference.get() + + +@contextmanager +def dispatch_as(reference): + token = _reference.set(reference) + try: + yield + finally: + _reference.reset(token) + + +def current_principal(reference=None): + from services.auth_service import AuthService + from services.key_controls import expired + + reference = reference or principal_reference() + try: + if not isinstance(reference, dict) or not reference.get("username"): + raise unavailable() + if str(reference["username"]).startswith("integration:"): + from services.intelligence_d1_store import request_private_intelligence + response = request_private_intelligence({"version": 1, "operation": "dispatch_policy", + "principalId": reference["username"]}, endpoint="auth") + if not isinstance(response, dict) or response.get("version") != 1 or set(response) != {"version", "principal"}: + raise unavailable() + user = response["principal"] + else: + user = AuthService._load_user_by_username(reference["username"]) + if not user or user.get("revoked_at") or expired(user): + raise unavailable() + if reference.get("api_key_prefix") and reference["api_key_prefix"] != user.get("api_key_prefix"): + raise unavailable() + policy = verify(user.get("data_policy")) + return {**user, **policy} + except Exception: + raise unavailable() from None + + +def capture_dispatch(callback, *, reference=None): + """Capture identity and application, never a policy snapshot, for a new thread.""" + reference = reference or principal_reference() + app = current_app._get_current_object() if has_app_context() else None + + def bound(*args, **kwargs): + token = _reference.set(reference) + try: + if app: + with app.app_context(): + return callback(*args, **kwargs) + return callback(*args, **kwargs) + finally: + _reference.reset(token) + return bound + + +def _credential(provider, user, url, options, *, public_asset=False): + headers = dict(options.get("headers") or {}) + if public_asset: + return {**options, "headers": strip_credentials(headers)} + if not has_data_controls(user): + return options + if any(name.lower() == "key" for name, _ in parse_qsl(urlsplit(url).query, keep_blank_values=True)) or any( + name.lower() == "key" for name in (options.get("params") or {})): + raise APIError("Caller provider credentials are not allowed", 403, {"error": "provider_credential_not_allowed"}) + if has_request_context() and (any(name.lower() == "key" for name in request.args) + or "x-goog-api-key" in request.headers): + raise APIError("Caller provider credentials are not allowed", 403, {"error": "provider_credential_not_allowed"}) + if has_request_context() and request.headers.get("X-MultiLLM-Api-Key"): + authorization = request.headers.get("Authorization") + if authorization and (authorization[7:].strip() if authorization.lower().startswith("bearer ") else authorization.strip()) != request.headers["X-MultiLLM-Api-Key"].strip(): + raise APIError("Caller provider credentials are not allowed", 403, {"error": "provider_credential_not_allowed"}) + # A private binding uses the operator's binding rather than a bearer credential. + if provider == "cloudflare" or urlsplit(url).hostname in {"ai.internal", "knowledge.internal", "media.internal"}: + return options + if not api_origin(provider, url): + raise unavailable_credential() + # Resolve only after current-policy admission, including OAuth token exchange. + with admitted_network(): + key = operator_credential(provider) + headers = strip_credentials(headers) + header = {"gemini": "x-goog-api-key", "anthropic": "x-api-key", "azure": "api-key"}.get(provider, "Authorization") + headers[header] = f"Bearer {key}" if header == "Authorization" else key + return {**options, "headers": headers} + + +def prepare_dispatch(provider, url, options, *, reference=None, binding=False, public_asset=False): + user = current_principal(reference) + if not binding and not model_permitted(user, provider + ":*"): + raise APIError("This API key cannot use this provider data class", 403, {"error": "provider_class_not_allowed"}) + options = _credential(provider, user, url, dict(options), public_asset=public_asset) + forced = preferences(user["forced_provider_preferences"]) + if provider == "openrouter" and forced is not None and (options.get("data") is not None or "json" in options): + try: + body = options["json"] if "json" in options else json.loads(options["data"]) + if not isinstance(body, dict): + raise ValueError() + except (ValueError, TypeError): + raise APIError("Enforced provider preferences require a JSON object", 400) from None + body = {**body, "provider": merge_preferences(body.get("provider"), forced)} + options = {**options, **({"json": body} if "json" in options else {"data": json.dumps(body).encode()})} + headers = dict(options.get("headers") or {}) + headers = {name: value for name, value in headers.items() if name.lower() not in {"content-length", "x-multillm-dispatch-principal"}} + if binding: + headers["X-MultiLLM-Dispatch-Principal"] = json.dumps({"version": 1, "username": user["username"]}) + return {**options, "headers": headers} + + +def provider_http(provider, method, url, *, transport=None, reference=None, binding=False, public_asset=False, **options): + options = prepare_dispatch(provider, url, options, reference=reference, binding=binding, public_asset=public_asset) + # Redirects are separate dispatches and must never forward an attested credential + # to a provider-selected host without another admission. + options["allow_redirects"] = False + with admitted_network(): + return (transport or requests.request)(method=method, url=url, **options) diff --git a/services/proxy_service.py b/services/proxy_service.py index e7b3b841..add230b4 100644 --- a/services/proxy_service.py +++ b/services/proxy_service.py @@ -193,8 +193,9 @@ def probe_nanogpt_key( } with requests.Session() as session: session.cookies.set_policy(_RejectAllCookiesPolicy()) - response = session.get( - f"{base_url.rstrip('/')}/v1/models", + from services.provider_dispatch import provider_http + response = provider_http("nanogpt", "GET", + f"{base_url.rstrip('/')}/v1/models", transport=session.request, headers=headers, params={"detailed": "false"}, timeout=(5, max(1, timeout_seconds)), @@ -1134,7 +1135,8 @@ def _make_request_with_timeout( headers: Dict[str, str], params: Dict[str, Any], data: Optional[bytes], - timeout: Tuple[int, int] + timeout: Tuple[int, int], + api_provider: str = "unknown", ) -> requests.Response: """ Make a single request with a given timeout (not widely used in code below). @@ -1156,7 +1158,8 @@ def _make_request_with_timeout( from services.admission_request import bounded_generation_timeout timeout = bounded_timeout(bounded_generation_timeout(timeout)) - response = session.request( + from services.provider_dispatch import provider_http + response = provider_http(api_provider, transport=session.request, method=method, url=url, headers=headers, @@ -1244,7 +1247,8 @@ def next_attempt(count: int) -> requests.Response: from services.admission_request import bounded_generation_timeout timeout = bounded_timeout(bounded_generation_timeout(timeout)) - response = session.request( + from services.provider_dispatch import provider_http + response = provider_http(api_provider, transport=session.request, method=method, url=url, headers=headers, @@ -2531,94 +2535,9 @@ def _handle_gemini_request( ) request_data.pop("webSearchSpec", None) - # Extract API key from URL parameters and rebuild the URL without it - api_key = None - - # Check if key is in params - if params and 'key' in params: - api_key = params.pop('key') - if isinstance(api_key, (list, tuple)): - api_key = api_key[0] if api_key else None - logger.info(f"Found API key in URL parameters for {api_provider}") - - # If no key in params, check if it's in the URL - else: - parsed_url = urlsplit(url) - query_pairs = parse_qsl(parsed_url.query, keep_blank_values=True) - filtered_query_pairs = [] - for key, value in query_pairs: - if key == 'key' and api_key is None: - api_key = value - logger.info(f"Found API key in URL for {api_provider}") - else: - filtered_query_pairs.append((key, value)) - - if api_key is not None: - url = urlunsplit( - ( - parsed_url.scheme, - parsed_url.netloc, - parsed_url.path, - urlencode(filtered_query_pairs), - parsed_url.fragment, - ) - ) - - # If still no API key, use the auth token passed from make_request - # But first check if it's the admin API key - if so, use the Gemini key from env instead - if not api_key and auth_token: - admin_api_key = os.environ.get('ADMIN_API_KEY') - if auth_token == admin_api_key: - # Using admin key, look up the actual provider key instead - logger.info(f"Admin API key detected - using {api_provider} API key from env instead") - api_key = AuthService.get_api_key(api_provider) - if not api_key: - # Try alternative keys - if api_provider == 'gemma': - api_key = AuthService.get_api_key('gemini') - elif api_provider == 'gemini': - api_key = AuthService.get_api_key('gemma') - else: - # Not the admin key, might be a direct Gemini key - api_key = auth_token - logger.info(f"Using API key from Authorization header for {api_provider}") - - # If still no API key, get from auth service - if not api_key: - # First try with the exact api_provider - api_key = AuthService.get_api_key(api_provider) - - # If using 'gemma' as provider but no gemma key found, try with 'gemini' as they share the same key - if not api_key and api_provider == 'gemma': - api_key = AuthService.get_api_key('gemini') - logger.info("Using Gemini API key for Gemma model") - - # If using 'gemini' as provider but no gemini key found, try with 'gemma' as a fallback - elif not api_key and api_provider == 'gemini': - api_key = AuthService.get_api_key('gemma') - logger.info("Using Gemma API key for Gemini model") - - if not api_key: - raise APIError(f"No API key found for {api_provider}. Please set {api_provider.upper()}_API_KEY in your .env file.", status_code=401) - - logger.info(f"Using API key from AuthService for {api_provider}") - - # Validate API key format - Gemini keys should start with "AIza" - if not api_key.startswith("AIza"): - logger.warning(f"API key for {api_provider} doesn't match expected format (should start with 'AIza')") - # Try to find a valid key from the environment - env_key = os.environ.get(f'{api_provider.upper()}_API_KEY') - if env_key and env_key.startswith("AIza"): - logger.info(f"Found valid {api_provider} API key format in environment, using it instead") - api_key = env_key - - headers["x-goog-api-key"] = api_key - - # Remove the Authorization header as Gemini doesn't use it - if 'Authorization' in headers: - headers.pop('Authorization') - logger.info("Removed Authorization header for Gemini API request") - + from services.gemini_credentials import prepare_credentials + url, headers, params = prepare_credentials(url, headers, params, api_provider, auth_token, AuthService) + # Extract model if it's in the URL path for direct model invocation # Format like /v1beta/models/gemini-1.5-pro:generateContent or /models/gemini-1.5-flash:generateContent model = None @@ -3372,6 +3291,11 @@ def make_request( """ Make a request with retries and error handling """ + from services.key_data_guards import guard_request + data = guard_request(api_provider, data) + from services.gemini_credentials import prepare_credentials, restricted + if restricted(api_provider): + url, headers, params = prepare_credentials(url, headers, params, api_provider, None, AuthService) headers = ( with_opencode_request_session(headers, data) if api_provider == "opencode" diff --git a/services/request_accounting.py b/services/request_accounting.py index e026251e..a638320b 100644 --- a/services/request_accounting.py +++ b/services/request_accounting.py @@ -271,6 +271,9 @@ def begin() -> Optional[Response]: return None payload = _payload() models, provider = _requested_models(kind, payload) + from services.key_data_guards import model_permitted + if any(not model_permitted(user, model) for model in models) or (provider and not model_permitted(user, provider + ":*")): + return _error(403, "provider_class_not_allowed", "This API key cannot use this provider data class.") for model in models: if not key_controls.model_allowed(user, model): return _error(403, "model_not_allowed", @@ -511,8 +514,7 @@ def _record(context: UsageContext, status: int, usage: Optional[UsageObservation def _row(context: UsageContext, status: int, usage: Optional[UsageObservation], units: Optional[int]) -> dict: if usage is not None and context.path == "/v1/embeddings": - usage = (replace(usage, output_tokens=0) if isinstance(usage, prompt_cache_cost.CacheObservation) - else UsageObservation(usage.input_tokens, 0, "provider", ("provider",))) + usage = replace(usage, output_tokens=0) qualified = [_qualified(model, context.provider) for model in context.models] requested = qualified[0] if len(qualified) == 1 else ("mixed" if qualified else None) selected = context.selected or (requested if requested and requested != "mixed" @@ -550,6 +552,8 @@ def _row(context: UsageContext, status: int, usage: Optional[UsageObservation], "latency_ms": min(86_400_000, max(0, round((time.perf_counter() - context.started) * 1000))), "input_tokens": usage.input_tokens if usage is not None else None, "output_tokens": usage.output_tokens if usage is not None else None, + "cached_input_tokens": usage.cached_input_tokens if usage is not None else None, + "reasoning_tokens": usage.reasoning_tokens if usage is not None else None, "cost_usd": cost, "cost_basis": basis, "request_id": context.request_id, diff --git a/services/shadow_eval_contract.py b/services/shadow_eval_contract.py index f098f6c5..7d82d962 100644 --- a/services/shadow_eval_contract.py +++ b/services/shadow_eval_contract.py @@ -8,6 +8,7 @@ import uuid from services.secret_scan import scan_payload +from services.key_data_guards import has_data_controls TASKS = ("coding", "extraction", "writing", "reasoning", "chat") REQUEST_BYTES = 65536 @@ -69,7 +70,7 @@ def task_type(payload): def eligible(payload, user, path, *, random_value=None): model = payload.get("model") rate = user.get("shadow_eval_rate") - return (path in {"/v1/chat/completions", "/v1/responses", "/intelligence/v1/chat/completions", + return (not has_data_controls(user) and path in {"/v1/chat/completions", "/v1/responses", "/intelligence/v1/chat/completions", "/v1/messages", "/optimize/v1/chat/completions"} and isinstance(payload.get("messages"), list) and ((isinstance(model, str) and model.startswith(("auto:", "cascade:"))) @@ -78,6 +79,8 @@ def eligible(payload, user, path, *, random_value=None): def make_sample(payload, user, route, answer, model, latency_ms, usage, *, now=None, finish_reason=None, on_skip=None): + if has_data_controls(user): + return None request = {name: payload[name] for name in REQUEST_FIELDS if name in payload} answer = {name: answer[name] for name in ("content", "tool_calls") if name in answer} def reject(reason): diff --git a/services/shadow_eval_data_policy.py b/services/shadow_eval_data_policy.py new file mode 100644 index 00000000..f5849715 --- /dev/null +++ b/services/shadow_eval_data_policy.py @@ -0,0 +1,30 @@ +"""Recheck source-key authority before retained content leaves the evaluation runner.""" + +from error_handlers import APIError +from services.key_data_guards import has_data_controls + + +def source_allows_replay(sample): + from services.auth_service import AuthService + from services.key_controls import expired + + try: + user = AuthService._load_user_by_username(sample["key_id"]) + from services.dispatch_policy import verify + policy = verify(user.get("data_policy") if user else None) + return bool(user and not user.get("revoked_at") and not expired(user) + and not has_data_controls(policy)) + except Exception: + # Missing, malformed or unavailable authority cannot authorize content replay. + return False + + +def guarded_dispatch(sample, dispatch): + def guarded(payload): + if not source_allows_replay(sample): + raise APIError("Source key does not permit shadow replay", 403, + {"error": "shadow_source_not_allowed"}) + from services.provider_dispatch import dispatch_as + with dispatch_as({"username": sample["key_id"]}): + return dispatch(payload) + return guarded diff --git a/services/shadow_eval_runner.py b/services/shadow_eval_runner.py index 63b24635..996b3dd4 100644 --- a/services/shadow_eval_runner.py +++ b/services/shadow_eval_runner.py @@ -15,6 +15,7 @@ from services.shadow_eval_contract import TASKS, clean_usage, encoded, finite from services.shadow_eval_judge import judge_payload, outcome, parse_judgment, random_order, tool_validity from services.shadow_eval_store import ShadowEvalStore +from services.shadow_eval_data_policy import guarded_dispatch, source_allows_replay from services import evaluation_noise_floor as noise_floor PRINCIPAL = "internal:shadow-evaluation" @@ -76,6 +77,7 @@ def replay_payload(sample, candidate): def evaluate(sample, candidate, config, dispatch, *, deadline): + dispatch = guarded_dispatch(sample, dispatch) result = {"sample_id": sample["id"], "task_type": sample["task_type"], "candidate_model": candidate, "candidate_route": candidate, "production_model": sample["production_model"], "outcome": "failed", "judge_model": config["judge_model"], "latencies": {"production": sample["latency_ms"], "candidate": 0, "judges": []}, @@ -146,7 +148,7 @@ def run(app, dispatch, *, store=ShadowEvalStore, options=None): if (count + 1) * units > config["max_replays_per_run"] or time.monotonic() >= deadline: return count sample = store.pending(task, model) - if not sample: + if not sample or not source_allows_replay(sample): continue identifier = uuid.uuid4().hex reserve = noise_floor.claim if options is not None else lambda storage, *args: storage.claim(*args) @@ -155,7 +157,7 @@ def run(app, dispatch, *, store=ShadowEvalStore, options=None): count += 1 progressed = True if options is not None: - result = noise_floor.evaluate_three_arm(sample, model, config, dispatch, deadline=deadline, + result = noise_floor.evaluate_three_arm(sample, model, config, guarded_dispatch(sample, dispatch), deadline=deadline, options=options, run_id=run_id, call=call) else: result = evaluate(sample, model, config, dispatch, deadline=deadline) diff --git a/services/stream_cost_breaker.py b/services/stream_cost_breaker.py index 93029ecc..8ff9f31f 100644 --- a/services/stream_cost_breaker.py +++ b/services/stream_cost_breaker.py @@ -78,7 +78,7 @@ def validated_control(payload) -> dict: return {CAP_FIELD: validate_cap(payload["max_stream_cost_usd"])} if enabled() and "max_stream_cost_usd" in payload else {} def sql_select(query: str) -> str: - return query.replace("shadow_eval_rate\n", "shadow_eval_rate, max_stream_cost_microusd\n") if enabled() else query + return query.replace("forced_provider_preferences\n", "forced_provider_preferences, max_stream_cost_microusd\n") if enabled() else query def ensure_sql_cap(connection) -> None: if enabled() and CAP_FIELD not in {row["name"] for row in connection.execute("PRAGMA table_info(users)")}: diff --git a/services/telemetry_export.py b/services/telemetry_export.py index 09433994..54b22aed 100644 --- a/services/telemetry_export.py +++ b/services/telemetry_export.py @@ -21,6 +21,8 @@ import requests +from services.dispatch_network import control_network + from services.observability_adapters import OBSERVABILITY_EXPORTER logger = logging.getLogger(__name__) @@ -138,8 +140,9 @@ def export_once(self) -> int: if not target: continue try: - response = requests.post(target, json=payload, headers=headers(signal), timeout=TIMEOUT, - allow_redirects=False) + with control_network(target): + response = requests.post(target, json=payload, headers=headers(signal), timeout=TIMEOUT, + allow_redirects=False) if response.status_code >= 300: raise ValueError(f"HTTP {response.status_code}") sent = True diff --git a/services/usage_store.py b/services/usage_store.py index b98c134a..ed7a3ba9 100644 --- a/services/usage_store.py +++ b/services/usage_store.py @@ -23,10 +23,13 @@ LATENCY_BUCKETS = (250, 500, 1000, 2000, 4000, 8000, 15000, 30000, 60000, 120000) BUCKET_COLUMNS = tuple(f"lat_le_{bound}" for bound in LATENCY_BUCKETS) + ("lat_gt_120000",) ROW_FIELDS = ("at", "principal", "key_prefix", "kind", "endpoint", "requested_model", "selected_model", - "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id") + "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id", "cached_input_tokens", "reasoning_tokens") EVENT_COLUMNS = ("id",) + ROW_FIELDS TOTAL_COLUMNS = ("requests", "errors", "input_tokens", "output_tokens", "cost_usd", "priced_requests", "latency_ms_total") + BUCKET_COLUMNS +DETAIL_COLUMNS = ("cached_input_tokens", "reasoning_tokens") +TOTAL_COLUMNS += DETAIL_COLUMNS +SUMMARY_COLUMNS = TOTAL_COLUMNS[:7] + DETAIL_COLUMNS GROUPS = ("day", "model", "principal") MAX_PAGE = 500 _BATCH_ID = re.compile(r"[0-9a-f]{32}\Z") @@ -76,6 +79,7 @@ def summarize(row: dict) -> dict: latency_avg_ms=round(int(row.get("latency_ms_total") or 0) / requests) if requests else None, latency_p50_ms=percentile(buckets, 0.5), latency_p95_ms=percentile(buckets, 0.95), + cache_hit_rate=round(100 * int(row.get("cached_input_tokens") or 0) / int(row.get("input_tokens") or 1), 1), ) return result @@ -131,7 +135,7 @@ def totals(self, principal: str, day: str, month_start: str) -> dict: def summary(self, group: str, since: str, until: str, principal: Optional[str], limit: int) -> list[dict]: response = self._call("summary", group=group, since=since, until=until, principal=principal, limit=limit) - fields = {group, *TOTAL_COLUMNS[:7], "latency_buckets"} + fields = {group, *SUMMARY_COLUMNS, "latency_buckets"} return _checked_rows(response.get("rows") if set(response) == {"version", "rows"} else None, fields) def recent(self, since: str, principal: Optional[str], before: Optional[int], limit: int) -> list[dict]: @@ -237,9 +241,24 @@ def ensure(connection) -> None: """ ) connection.execute("CREATE INDEX IF NOT EXISTS idx_usage_daily_principal ON usage_daily(principal, day)") + SqlUsageStore.ensure_details(connection) if prompt_cache_cost.enabled(): SqlUsageStore.ensure_buckets(connection) + @staticmethod + def ensure_details(connection) -> None: + for table in ("usage_events", "usage_daily"): + if getattr(connection, "dialect", None) == "postgresql": + columns = {row["name"] for row in connection.execute( + "SELECT column_name AS name FROM information_schema.columns " + "WHERE table_schema = 'multillm' AND table_name = ?", (table,))} + else: + columns = {row[1] for row in connection.execute(f"PRAGMA table_info({table})")} # nosec B608 + for name in DETAIL_COLUMNS: + if name not in columns: + suffix = " NOT NULL DEFAULT 0" if table == "usage_daily" else "" + connection.execute(f"ALTER TABLE {table} ADD COLUMN {name} INTEGER{suffix}") # nosec B608 + def record(self, batch_id: str, rows: list[dict]) -> int: if not _BATCH_ID.fullmatch(batch_id): raise UsageStoreError("invalid batch") @@ -255,6 +274,8 @@ def record(self, batch_id: str, rows: list[dict]) -> int: totals[5] += int(row["cost_usd"] is not None) totals[6] += row["latency_ms"] totals[7 + latency_bucket(row["latency_ms"])] += 1 + for name in DETAIL_COLUMNS: + totals[TOTAL_COLUMNS.index(name)] += row.get(name) or 0 row_fields = ROW_FIELDS + (prompt_cache_cost.ROW_FIELDS if prompt_cache_cost.enabled() else ()) columns = ", ".join(TOTAL_COLUMNS) updates = ", ".join(f"{name} = usage_daily.{name} + excluded.{name}" for name in TOTAL_COLUMNS) @@ -264,7 +285,7 @@ def record(self, batch_id: str, rows: list[dict]) -> int: connection.executemany( f"INSERT INTO usage_events (day, {', '.join(row_fields)}) " # nosec B608 f"VALUES ({', '.join('?' for _ in range(len(row_fields) + 1))})", - [(row["at"][:10], *(row[name] if name in ROW_FIELDS else row.get(name) for name in row_fields)) for row in rows], + [(row["at"][:10], *(row.get(name) for name in row_fields)) for row in rows], ) # Column names come only from the fixed TOTAL_COLUMNS definition. connection.executemany( @@ -311,7 +332,7 @@ def summary(self, group: str, since: str, until: str, principal: Optional[str], f"GROUP BY {group} ORDER BY {order}, {group} LIMIT ?", parameters, ) - return [{group: row[group], **{name: row[name] or 0 for name in TOTAL_COLUMNS[:7]}, + return [{group: row[group], **{name: row[name] or 0 for name in SUMMARY_COLUMNS}, "latency_buckets": [int(row[name] or 0) for name in BUCKET_COLUMNS]} for row in rows] def recent(self, since: str, principal: Optional[str], before: Optional[int], limit: int) -> list[dict]: diff --git a/services/usage_types.py b/services/usage_types.py index 1ec3936b..32d32f7a 100644 --- a/services/usage_types.py +++ b/services/usage_types.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Literal UsageBasis = Literal["provider", "estimated", "unknown"] @@ -19,6 +19,8 @@ class UsageObservation: output_tokens: int | None = None basis: UsageBasis = "unknown" provenance: tuple[str, ...] = () + cached_input_tokens: int | None = field(default=None, kw_only=True) + reasoning_tokens: int | None = field(default=None, kw_only=True) @classmethod def from_body(cls, value: Any) -> UsageObservation | None: @@ -35,12 +37,21 @@ def from_body(cls, value: Any) -> UsageObservation | None: return None input_tokens = _component(usage.get("prompt_tokens", usage.get("input_tokens"))) output_tokens = _component(usage.get("completion_tokens", usage.get("output_tokens"))) + input_details = usage.get("prompt_tokens_details", usage.get("input_tokens_details")) + output_details = usage.get("completion_tokens_details", usage.get("output_tokens_details")) + cached = _component(input_details.get("cached_tokens")) if isinstance(input_details, dict) else None + reasoning = _component(output_details.get("reasoning_tokens")) if isinstance(output_details, dict) else None + if "cache_read_input_tokens" in usage or "cache_creation_input_tokens" in usage: + cached = _component(usage.get("cache_read_input_tokens")) + written = _component(usage.get("cache_creation_input_tokens")) + if input_tokens is not None: + input_tokens += (cached or 0) + (written or 0) data = value.get("data") if isinstance(data, list) and any(isinstance(item, dict) and "embedding" in item for item in data): output_tokens = 0 known = input_tokens is not None or output_tokens is not None return cls(input_tokens, output_tokens, "provider" if known else "unknown", - ("provider",) if known else ()) + ("provider",) if known else (), cached_input_tokens=cached, reasoning_tokens=reasoning) def merge(self, later: UsageObservation) -> UsageObservation: """Use the latest valid value for each component; null never erases a count.""" @@ -49,7 +60,9 @@ def merge(self, later: UsageObservation) -> UsageObservation: provenance = tuple(dict.fromkeys((*self.provenance, *later.provenance))) basis: UsageBasis = "estimated" if "request_estimate" in provenance else ( "provider" if input_tokens is not None or output_tokens is not None else "unknown") - return UsageObservation(input_tokens, output_tokens, basis, provenance) + return UsageObservation(input_tokens, output_tokens, basis, provenance, + cached_input_tokens=later.cached_input_tokens if later.cached_input_tokens is not None else self.cached_input_tokens, + reasoning_tokens=later.reasoning_tokens if later.reasoning_tokens is not None else self.reasoning_tokens) def with_estimates(self, input_tokens: int | None, output_tokens: int | None) -> UsageObservation: """Fill missing components explicitly, retaining measured zero and provenance.""" @@ -59,7 +72,8 @@ def with_estimates(self, input_tokens: int | None, output_tokens: int | None) -> return self return UsageObservation(self.input_tokens if self.input_tokens is not None else estimated_input, self.output_tokens if self.output_tokens is not None else estimated_output, - "estimated", tuple(dict.fromkeys((*self.provenance, "request_estimate")))) + "estimated", tuple(dict.fromkeys((*self.provenance, "request_estimate"))), + cached_input_tokens=self.cached_input_tokens, reasoning_tokens=self.reasoning_tokens) def storage_basis(self, cost: float | None) -> str | None: """Map internal provenance to the existing strict SQL/D1 cost-basis vocabulary.""" diff --git a/services/user_store.py b/services/user_store.py index 7e9224a6..1ce5513c 100644 --- a/services/user_store.py +++ b/services/user_store.py @@ -20,11 +20,11 @@ "last_login", "last_used_at", "last_used_ip", "created_by", "rotated_at", "revoked_at", # Per-key controls (migration 0007); NULL means no budget, every model, no expiry, any address. "daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", - "shadow_eval_rate", + "shadow_eval_rate", "official_only", "forced_provider_preferences", ) _REQUIRED_TEXT = ("username", "api_key_hash", "api_key_prefix", "scopes", "created_at") _OPTIONAL_TEXT = ("last_login", "last_used_at", "last_used_ip", "created_by", "rotated_at", "revoked_at", - "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode") + "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "forced_provider_preferences") _OPTIONAL_NUMBER = ("daily_budget_usd", "monthly_budget_usd", "shadow_eval_rate") PAGE_SIZE = 200 READ_OPERATIONS = frozenset({"list", "get", "by_prefix"}) @@ -61,7 +61,7 @@ def _call(operation, **values): attempts = 2 if operation in READ_OPERATIONS else 1 for attempt in range(1, attempts + 1): try: - return request_private_intelligence({"operation": operation, **values}, endpoint="users") + return request_private_intelligence({"operation": operation, "data_policy_version": 1, **values}, endpoint="users") except Exception as error: cause = (f"{error.status} {error.code}" if isinstance(error, PrivateIntelligenceError) else getattr(error, "code", None) or type(error).__name__) @@ -79,6 +79,11 @@ def _call(operation, **values): def _row(value): if isinstance(value, dict): # A Worker released before migrations 0011 and 0013 returns rows without these columns. + from services.dispatch_policy import envelope, verify + policy = verify(value.get("data_policy")) + value = {name: item for name, item in value.items() if name != "data_policy"} + if envelope(value) != policy: + raise unavailable() value = {"secret_scan_mode": None, "shadow_eval_rate": None, **value} # nosec B105 - column defaults if ( not isinstance(value, dict) @@ -87,6 +92,7 @@ def _row(value): or any(value[name] is not None and not isinstance(value[name], str) for name in _OPTIONAL_TEXT) or any(value[name] is not None and (isinstance(value[name], bool) or not isinstance(value[name], (int, float))) for name in _OPTIONAL_NUMBER) + or (value["official_only"] is not None and (type(value["official_only"]) is not int or value["official_only"] not in (0, 1))) or type(value["is_admin"]) is not int or value["is_admin"] not in (0, 1) ): diff --git a/services/video_generation.py b/services/video_generation.py index b4c0b32f..85089594 100644 --- a/services/video_generation.py +++ b/services/video_generation.py @@ -150,14 +150,15 @@ def _failure(status: int, message: str, transport: str | None = None) -> Respons return response -def _send(method: str, url: str, *, timeout, **kwargs) -> requests.Response | Response: +def _send(method: str, url: str, *, provider: str, timeout, **kwargs) -> requests.Response | Response: """One provider call without retries: a sent job may already be accepted and billed.""" if "json" in kwargs: kwargs["json"] = protect_payload(kwargs["json"], provider="video") if "data" in kwargs: kwargs["data"] = protect_body(kwargs["data"], kwargs.get("headers"), provider="video") try: - return requests.request(method, url, timeout=timeout, allow_redirects=False, **kwargs) + from services.provider_dispatch import provider_http + return provider_http(provider, method, url, timeout=timeout, **kwargs) except requests.exceptions.ConnectTimeout: return _failure(502, "The video provider could not be reached", "connect") except requests.exceptions.ConnectionError as error: @@ -186,14 +187,14 @@ def create_job(provider: str, model: str, request: VideoRequest, key: str | None "seconds": str(_nearest(request.seconds, (4, 8, 12)))} if request.image_url: body["input_reference"] = {"image_url": request.image_url} - response = _send("POST", f"{base_url.rstrip('/')}/v1/videos", timeout=CREATE_TIMEOUT, json=body, + response = _send("POST", f"{base_url.rstrip('/')}/v1/videos", provider=provider, timeout=CREATE_TIMEOUT, json=body, headers={"Authorization": f"Bearer {key}"}) elif provider == "xai": body = {"model": model, "prompt": request.prompt, "duration": min(15, request.seconds), "aspect_ratio": request.aspect_ratio, "resolution": request.resolution} if request.image_url: body["image"] = {"url": request.image_url} - response = _send("POST", f"{base_url.rstrip('/')}/v1/videos/generations", timeout=CREATE_TIMEOUT, json=body, + response = _send("POST", f"{base_url.rstrip('/')}/v1/videos/generations", provider=provider, timeout=CREATE_TIMEOUT, json=body, headers={"Authorization": f"Bearer {key}"}) elif provider == "gemini": instance: dict[str, object] = {"prompt": request.prompt} @@ -204,7 +205,7 @@ def create_job(provider: str, model: str, request: VideoRequest, key: str | None body = {"instances": [instance], "parameters": { "sampleCount": 1, "aspectRatio": aspect_ratio, "resolution": request.resolution, "durationSeconds": _nearest(request.seconds, (4, 6, 8))}} - response = _send("POST", f"{GEMINI_BASE}/models/{model}:predictLongRunning", timeout=CREATE_TIMEOUT, + response = _send("POST", f"{GEMINI_BASE}/models/{model}:predictLongRunning", provider=provider, timeout=CREATE_TIMEOUT, json=body, headers={"x-goog-api-key": key}) elif provider == "cloudflare": if not cloudflare_ai.enabled(): @@ -240,10 +241,10 @@ def job_status(job: dict, job_id: str, key: str | None, base_url: str) -> dict: if provider == "cloudflare": return {**public, "status": "completed", "progress": 100} if provider in ("openai", "xai"): - response = _send("GET", f"{base_url.rstrip('/')}/v1/videos/{upstream}", timeout=POLL_TIMEOUT, + response = _send("GET", f"{base_url.rstrip('/')}/v1/videos/{upstream}", provider=provider, timeout=POLL_TIMEOUT, headers={"Authorization": f"Bearer {key}"}) else: - response = _send("GET", f"{GEMINI_BASE}/{upstream}", timeout=POLL_TIMEOUT, headers={"x-goog-api-key": key}) + response = _send("GET", f"{GEMINI_BASE}/{upstream}", provider=provider, timeout=POLL_TIMEOUT, headers={"x-goog-api-key": key}) if isinstance(response, Response) or response.status_code >= 500: # A failed status read is not a failed job: report it and let the caller poll again. return {**public, "status": "in_progress", "poll_error": "The provider status could not be read; poll again"} @@ -275,13 +276,13 @@ def content_source(job: dict, key: str | None, base_url: str) -> tuple[str, dict if provider == "openai": return f"{base_url.rstrip('/')}/v1/videos/{upstream}/content", {"Authorization": f"Bearer {key}"} if provider == "xai": - response = _send("GET", f"{base_url.rstrip('/')}/v1/videos/{upstream}", timeout=POLL_TIMEOUT, + response = _send("GET", f"{base_url.rstrip('/')}/v1/videos/{upstream}", provider=provider, timeout=POLL_TIMEOUT, headers={"Authorization": f"Bearer {key}"}) url = ((_json(response).get("video") or {}).get("url") if not isinstance(response, Response) else None) if not url: raise APIError("The video is not ready", status_code=409) return url, {} - response = _send("GET", f"{GEMINI_BASE}/{upstream}", timeout=POLL_TIMEOUT, headers={"x-goog-api-key": key}) + response = _send("GET", f"{GEMINI_BASE}/{upstream}", provider=provider, timeout=POLL_TIMEOUT, headers={"x-goog-api-key": key}) samples = (((_json(response).get("response") or {}).get("generateVideoResponse") or {}).get("generatedSamples") if not isinstance(response, Response) else None) or [] uri = ((samples[0] or {}).get("video") or {}).get("uri") if samples else None @@ -290,14 +291,15 @@ def content_source(job: dict, key: str | None, base_url: str) -> tuple[str, dict return uri, {"x-goog-api-key": key} -def stream_content(url: str, headers: dict) -> Response: +def stream_content(url: str, headers: dict, *, provider: str) -> Response: if not url.startswith("https://"): raise APIError("The provider returned an unsupported video location", status_code=502) try: - upstream = requests.get(url, headers=headers, timeout=DOWNLOAD_TIMEOUT, stream=True, allow_redirects=True) + from services.provider_assets import asset_http + upstream = asset_http(provider, url, headers=headers, timeout=DOWNLOAD_TIMEOUT, stream=True) except requests.exceptions.RequestException: raise APIError("The video could not be downloaded; try again", status_code=502) from None - if upstream.status_code >= 400: + if upstream.status_code != 200: upstream.close() raise APIError("The video is not available", status_code=409 if upstream.status_code in (404, 409) else 502) length = int(upstream.headers.get("Content-Length") or 0) diff --git a/static/js/key-controls.js b/static/js/key-controls.js index 4cf9c788..70a79916 100644 --- a/static/js/key-controls.js +++ b/static/js/key-controls.js @@ -5,6 +5,8 @@ /** The PUT /users//controls body for the dialog's field values. */ function controlsPayload(fields) { return { + official_only: fields.official_only === "on" || fields.official_only === true, + forced_provider_preferences: fields.forced_provider_preferences ? JSON.parse(fields.forced_provider_preferences) : null, secret_scan_mode: fields.secret_scan_mode || null, shadow_eval_rate: amount(fields.shadow_eval_rate), daily_budget_usd: amount(fields.daily_budget_usd), @@ -43,6 +45,8 @@ controls = {}; } document.getElementById('key-controls-user').textContent = username; + field('official_only').checked = controls.official_only === true; + field('forced_provider_preferences').value = controls.forced_provider_preferences ? JSON.stringify(controls.forced_provider_preferences, null, 2) : ''; field('daily_budget_usd').value = controls.daily_budget_usd ?? ''; field('monthly_budget_usd').value = controls.monthly_budget_usd ?? ''; field('allowed_models').value = (controls.allowed_models || []).join('\n'); diff --git a/static/js/usage.js b/static/js/usage.js index 80ed6135..b66739f4 100644 --- a/static/js/usage.js +++ b/static/js/usage.js @@ -113,7 +113,8 @@ ], 'No billable requests in this range.'); fillTable('usage-models', data.models || [], [ (row) => row.model, (row) => formatNumber(row.requests), formatErrors, - (row) => `${formatNumber(row.input_tokens)} / ${formatNumber(row.output_tokens)}`, (row) => formatCost(row.cost_usd), ...latency, + (row) => `${formatNumber(row.input_tokens)} / ${formatNumber(row.output_tokens)}`, + (row) => `${Number(row.cache_hit_rate || 0).toFixed(1)}%`, (row) => formatNumber(row.reasoning_tokens), (row) => formatCost(row.cost_usd), ...latency, ], 'No billable requests in this range.'); if (isAdmin) { const keysPanel = document.getElementById('usage-keys-panel'); @@ -122,7 +123,8 @@ } const principals = data.principals || []; fillTable('usage-keys', principals, [ - (row) => keyButton(row.principal), (row) => formatNumber(row.requests), formatErrors, (row) => formatCost(row.cost_usd), ...latency, + (row) => keyButton(row.principal), (row) => `${Number(row.cache_hit_rate || 0).toFixed(1)}%`, + (row) => formatNumber(row.reasoning_tokens), (row) => formatNumber(row.requests), formatErrors, (row) => formatCost(row.cost_usd), ...latency, ], 'No billable requests in this range.'); principals.forEach((row) => { if (principalSelect && !knownPrincipals.has(row.principal)) { diff --git a/templates/usage.html b/templates/usage.html index ad1f9f55..3ef70ca5 100644 --- a/templates/usage.html +++ b/templates/usage.html @@ -83,7 +83,7 @@

Filters

By model

The model that served each request; automatic routes show their selected candidate.

- +
ModelRequestsErrorsTokens in / outCost (USD)p50 msp95 ms
ModelRequestsErrorsTokens in / outCache hit %Reasoning tokensCost (USD)p50 msp95 ms
@@ -94,7 +94,7 @@

Filters

By key

Select a key to see its budget and controls.

- +
KeyRequestsErrorsCost (USD)p50 msp95 ms
KeyCache hit %Reasoning tokensRequestsErrorsCost (USD)p50 msp95 ms
@@ -104,5 +104,5 @@

Filters

{% endblock %} {% block scripts %} - + {% endblock %} diff --git a/templates/users.html b/templates/users.html index a8c8a4c3..f3b9d58e 100644 --- a/templates/users.html +++ b/templates/users.html @@ -135,7 +135,7 @@

Accounts

+ +

Exclude subscription, reseller and free providers, including routed candidates.

+ @@ -216,5 +221,5 @@

Save this API key now

{% block scripts %} - + {% endblock %} diff --git a/tests/conftest.py b/tests/conftest.py index 3afb448c..d6bdb5ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,20 @@ import pytest +def pytest_sessionstart(session): + """Audit the extension socket too, including connections from alternate clients.""" + import sys + from services.dispatch_network import network_admitted + + def audit(event, args): + if event == "socket.connect" and not network_admitted(args[1]): + raise AssertionError("Outbound connection outside provider chokepoint") + + # Audit hooks cannot be removed. Keep enforcement active for this process, + # including collection, fixture teardown and background completions. + sys.addaudithook(audit) + + @pytest.fixture(autouse=True) def _fresh_public_capability_cache(): # The models.dev index is cached per process; each test sees its own fixtures. @@ -24,3 +38,22 @@ def _fresh_public_capability_cache(): reset_public_capability_cache() yield reset_public_capability_cache() + + +@pytest.fixture(autouse=True) +def _outbound_connections_require_dispatch(monkeypatch): + """All HTTP clients, including unlisted modules and SDKs, meet this boundary.""" + import socket + from services.dispatch_network import network_admitted + + def guard(original): + def checked(*args, **kwargs): + address = args[1] if args and isinstance(args[0], socket.socket) else (args[0] if args else kwargs.get("address")) + if not network_admitted(address): + raise AssertionError("Outbound connection outside provider chokepoint") + return original(*args, **kwargs) + return checked + + monkeypatch.setattr(socket, "create_connection", guard(socket.create_connection)) + monkeypatch.setattr(socket.socket, "connect", guard(socket.socket.connect)) + monkeypatch.setattr(socket.socket, "connect_ex", guard(socket.socket.connect_ex)) diff --git a/tests/dispatch_fixture.py b/tests/dispatch_fixture.py new file mode 100644 index 00000000..d0a5bf23 --- /dev/null +++ b/tests/dispatch_fixture.py @@ -0,0 +1,20 @@ +"""Explicit durable identity for isolated provider transport tests.""" + +import pytest + +from services.dispatch_policy import envelope +from services.provider_dispatch import dispatch_as + + +@pytest.fixture +def verified_transport_principal(monkeypatch): + from services.auth_service import AuthService + + # Exercise real admission with an explicitly unrestricted stored account. + # Policy and authentication regressions use their own stores instead. + monkeypatch.setattr(AuthService, "_load_user_by_username", lambda username: { + "username": username, + "data_policy": envelope({"official_only": None, "forced_provider_preferences": None}), + }) + with dispatch_as({"username": "synthetic-transport"}): + yield diff --git a/tests/fixtures/secret_firewall_coverage.json b/tests/fixtures/secret_firewall_coverage.json index 02dccef4..ca435815 100644 --- a/tests/fixtures/secret_firewall_coverage.json +++ b/tests/fixtures/secret_firewall_coverage.json @@ -262,7 +262,7 @@ "guards": 3 }, "worker/roleplay/transport.mjs": { - "dispatches": 4, + "dispatches": 3, "guards": 1 }, "worker/knowledge-edge.mjs": { @@ -284,6 +284,7 @@ }, "edge_modules": [ "worker/access-sso.mjs", + "worker/account-contract.mjs", "worker/admission-do.mjs", "worker/ai-outbound.mjs", "worker/alert-delivery.mjs", @@ -307,6 +308,8 @@ "worker/credits-admission.mjs", "worker/credits-d1.mjs", "worker/d1-schema.mjs", + "worker/dispatch-network.mjs", + "worker/dispatch-policy.mjs", "worker/enterprise-contract.mjs", "worker/exact-generation-cache.mjs", "worker/fallback-page.mjs", @@ -321,6 +324,7 @@ "worker/intelligence-d1.mjs", "worker/intelligence-outbound.mjs", "worker/janitor-reasoning-response.mjs", + "worker/key-data-guards.mjs", "worker/knowledge-edge.mjs", "worker/knowledge-outbound.mjs", "worker/knowledge/alexandria/catalogue.mjs", @@ -390,6 +394,7 @@ "worker/prometheus.mjs", "worker/prompt-injection-detection.mjs", "worker/prompt-templates-d1.mjs", + "worker/provider-dispatch.mjs", "worker/realtime-metering.mjs", "worker/realtime-session.mjs", "worker/realtime.mjs", diff --git a/tests/helpers/dispatch_fixture.mjs b/tests/helpers/dispatch_fixture.mjs new file mode 100644 index 00000000..868718db --- /dev/null +++ b/tests/helpers/dispatch_fixture.mjs @@ -0,0 +1,27 @@ +/** Explicit durable controls for transport tests; never a runtime fallback. */ +export function policyStore(controls = { official_only: null, forced_provider_preferences: null }) { + return { prepare(query) { + if (!query.startsWith("SELECT official_only, forced_provider_preferences FROM control_users")) throw Error(`Unexpected policy query: ${query}`); + return { bind() { return this; }, async first() { return structuredClone(controls); } }; + } }; +} +export const dispatchEnv = () => ({ INTELLIGENCE_DB: policyStore() }); + +const policyEnvironments = new WeakSet(); + +/** Independent middleware tests supply a durable policy without emulating unrelated SQL. */ +export function policyFixtureEnv(env = {}) { + if (policyEnvironments.has(env)) return env; + const db = env.INTELLIGENCE_DB; + const policy = policyStore(); + const bound = { ...env, INTELLIGENCE_DB: { ...db, prepare(query) { + if (query.startsWith("SELECT official_only, forced_provider_preferences FROM control_users") + || query.startsWith("SELECT official_only, forced_provider_preferences FROM intelligence_principals")) { + return policy.prepare(query.replace("intelligence_principals", "control_users")); + } + if (!db) throw Error(`Unexpected fixture SQL: ${query}`); + return db.prepare(query); + } } }; + policyEnvironments.add(bound); + return bound; +} diff --git a/tests/helpers/load_cloudflare_worker.mjs b/tests/helpers/load_cloudflare_worker.mjs index eafa4e70..141a64e0 100644 --- a/tests/helpers/load_cloudflare_worker.mjs +++ b/tests/helpers/load_cloudflare_worker.mjs @@ -1,6 +1,8 @@ import { readFile } from "node:fs/promises"; +export const { policyStore } = await import(new URL("./dispatch_fixture.mjs", import.meta.url)); function dataModuleUrl(source) { + source = source.replaceAll('from "../provider-dispatch.mjs"', `from "${new URL("../../worker/provider-dispatch.mjs", import.meta.url)}"`); return `data:text/javascript;base64,${Buffer.from(source, "utf8").toString("base64")}`; } diff --git a/tests/helpers/local_miniflare.mjs b/tests/helpers/local_miniflare.mjs new file mode 100644 index 00000000..44155c7d --- /dev/null +++ b/tests/helpers/local_miniflare.mjs @@ -0,0 +1,19 @@ +/** Register only each disposable D1 server's exact loopback port with the guard. */ +export * from "miniflare"; +import { Miniflare as NativeMiniflare } from "miniflare"; +import { registerFixtureOrigin } from "./network_guard.mjs"; +export class Miniflare extends NativeMiniflare { + constructor(...args) { + super(...args); + this.fixtureRegistration = this.ready.then(registerFixtureOrigin); + } + async getD1Database(...args) { + await this.fixtureRegistration; + return super.getD1Database(...args); + } + async dispose() { + const unregister = await this.fixtureRegistration.catch(() => null); + try { return await super.dispose(); } + finally { unregister?.(); } + } +} diff --git a/tests/helpers/network_fixture.mjs b/tests/helpers/network_fixture.mjs new file mode 100644 index 00000000..18a4b07e --- /dev/null +++ b/tests/helpers/network_fixture.mjs @@ -0,0 +1,7 @@ +import { controlNetwork } from "../../worker/dispatch-network.mjs"; +export function mockFetch(t, implementation) { + const previous = globalThis.fetch; + globalThis.fetch = implementation; + t.after(() => { globalThis.fetch = previous; }); +} +export const clientNetwork = (url, run) => controlNetwork(url, run); diff --git a/tests/helpers/network_guard.mjs b/tests/helpers/network_guard.mjs new file mode 100644 index 00000000..8dc9630b --- /dev/null +++ b/tests/helpers/network_guard.mjs @@ -0,0 +1,39 @@ +import net from "node:net"; +/** Suite-wide runtime guard; replacing fetch cannot replace the admission check. */ +import { networkAdmitted, connectionAdmitted } from "../../worker/dispatch-network.mjs"; +const guarded = new WeakSet(); +function wrap(transport) { + if (guarded.has(transport)) return transport; + const checked = function (...args) { + if (!networkAdmitted(args[0])) throw new Error("Outbound connection outside provider chokepoint"); + return transport.apply(this, args); + }; + guarded.add(checked); + return checked; +} +let transport = wrap(globalThis.fetch); +Object.defineProperty(globalThis, "fetch", { configurable: false, get: () => transport, + set: value => { transport = wrap(value); } }); +if (typeof globalThis.connect === "function") { + let connect = wrap(globalThis.connect); + Object.defineProperty(globalThis, "connect", { configurable: false, get: () => connect, + set: value => { connect = wrap(value); } }); +} + +// Only explicitly registered local D1 bridge ports are infrastructure traffic. +const fixturePorts = new Set(); +export function registerFixtureOrigin(url) { + const target = new URL(url); + if (!["127.0.0.1", "[::1]", "localhost"].includes(target.hostname)) throw Error("Non-local test fixture"); + fixturePorts.add(String(target.port)); + return () => fixturePorts.delete(String(target.port)); +} +const socketConnect = net.Socket.prototype.connect; +net.Socket.prototype.connect = function (...args) { + const options = Array.isArray(args[0]) ? args[0][0] : args[0]; + const host = typeof options === "object" ? options.host : args[1]; + const port = typeof options === "object" ? options.port : options; + const fixtureRpc = ["127.0.0.1", "::1", "localhost"].includes(host) && fixturePorts.has(String(port)); + if (!connectionAdmitted(host, port) && !fixtureRpc) throw new Error("Outbound connection outside provider chokepoint"); + return socketConnect.apply(this, args); +}; diff --git a/tests/helpers/policy_test.mjs b/tests/helpers/policy_test.mjs new file mode 100644 index 00000000..1363fb17 --- /dev/null +++ b/tests/helpers/policy_test.mjs @@ -0,0 +1,10 @@ +/** Unit transports have an explicit authenticated durable account. */ +import nativeTest from "node:test"; +import { withDispatchPrincipal } from "../../worker/provider-dispatch.mjs"; +import { dispatchEnv } from "./dispatch_fixture.mjs"; +function policyTest(name, ...args) { + const callback = args.pop(); + return nativeTest(name, ...args, (...values) => withDispatchPrincipal(dispatchEnv(), + { authenticated: true, keyId: "fixture-reader" }, () => callback(...values))); +} +export default Object.assign(policyTest, nativeTest); diff --git a/tests/helpers/provider_binding.mjs b/tests/helpers/provider_binding.mjs new file mode 100644 index 00000000..baa96fbb --- /dev/null +++ b/tests/helpers/provider_binding.mjs @@ -0,0 +1,28 @@ +/** Build every provider SDK/binding double here, including nested SDK handles. */ +import assert from "node:assert/strict"; +import { networkAdmitted } from "../../worker/dispatch-network.mjs"; + +export function providerBinding(binding) { + const wrapped = new WeakMap(); + function guard(value) { + if (!value || Array.isArray(value) || typeof value !== "object" && typeof value !== "function") return value; + if (wrapped.has(value)) return wrapped.get(value); + const proxy = new Proxy(value, { + get(target, name, receiver) { + const member = Reflect.get(target, name, receiver); + return name === "constructor" ? member : guard(member); + }, + apply(target, receiver, args) { + assert.ok(networkAdmitted(), "Provider binding outside provider chokepoint"); + const result = Reflect.apply(target, receiver, args); + // Synchronous SDK factories return handles whose methods need their own + // admission check. Async responses and streams remain ordinary results. + return result && Object.getPrototypeOf(result) === Object.prototype ? guard(result) : result; + }, + }); + wrapped.set(value, proxy); + wrapped.set(proxy, proxy); + return proxy; + } + return guard(binding); +} diff --git a/tests/helpers/roleplay_fixture.mjs b/tests/helpers/roleplay_fixture.mjs index a1f3aada..1862c397 100644 --- a/tests/helpers/roleplay_fixture.mjs +++ b/tests/helpers/roleplay_fixture.mjs @@ -1,4 +1,4 @@ -import { loadRoleplayModule } from "./load_cloudflare_worker.mjs"; +import { loadRoleplayModule, policyStore } from "./load_cloudflare_worker.mjs"; const { RoleplaySession, @@ -60,6 +60,7 @@ export function makeRoleplayEnv(overrides = {}) { const storageBySession = new Map(); const waits = []; const env = { + INTELLIGENCE_DB: policyStore(), ADMIN_API_KEY: "admin-roleplay-key", ROLEPLAY_API_KEY: "janitor-roleplay-key", OPENCODE_GO_API_KEY: "opencode-roleplay-key", diff --git a/tests/knowledge_fixture.mjs b/tests/knowledge_fixture.mjs index 5a08500e..d7ff2953 100644 --- a/tests/knowledge_fixture.mjs +++ b/tests/knowledge_fixture.mjs @@ -1,3 +1,4 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import { KnowledgeAuthority } from "../worker/knowledge/authority.mjs"; import { createArtifact } from "../worker/knowledge/evidence.mjs"; import { defaultPolicy } from "../worker/knowledge/policy.mjs"; @@ -63,7 +64,7 @@ export async function fixture() { async match(key) { return cacheEntries.get(key.url)?.clone(); }, async put(key, value) { cacheEntries.set(key.url, value.clone()); }, }; - const env = { KNOWLEDGE_SNAPSHOTS: {}, KNOWLEDGE_INDEX: {}, EXA_API_KEY: "synthetic-test-key" }; + const env = { INTELLIGENCE_DB: policyStore(), KNOWLEDGE_SNAPSHOTS: {}, KNOWLEDGE_INDEX: {}, EXA_API_KEY: "synthetic-test-key" }; const retrieve = async (provider, intent, { invoke }) => invoke(provider, "lookup", async () => { counts.providers++; return { observations: [{ kind: "source_excerpt", url: source.url, text, provider, freshness: "live" }], warnings: [] }; diff --git a/tests/outbound_inventory.json b/tests/outbound_inventory.json new file mode 100644 index 00000000..336eb0cc --- /dev/null +++ b/tests/outbound_inventory.json @@ -0,0 +1,2427 @@ +{ + "app.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "38480cbcaf363f62177f1449b73b12c6c1b379dbb938e0096d6ff3eebb65921e" + }, + "cloudflare-worker.mjs": { + "kind": "internal", + "reason": "Registered Container forwarding; provider I/O reenters guarded Flask or Worker adapters.", + "sha256": "b140cee4640aebfb2c2f6380ee04eeedda866632df7d370f2b0c2bf691911a1c" + }, + "config.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "98e753d318ac39e0d6e3b1c5c9feb561faee62b97515055b5c05859bd3c6953f" + }, + "env_loader.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ddc4d2ed5a4acc6c93c57e35371119a6bda3965a8ba59ff4aa96522f623d621a" + }, + "error_handlers.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8c8009220a718dfc3d4b1032c395f1c6fe32e639f208d6f81b1588d88e867493" + }, + "index.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "83932c6dc1e6e3cf822217dba6a1a575bfc983ac53842d1bec9a1512a8332782" + }, + "postcss.config.js": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d96a4fef9d253f0526603b86935d8e41f0731cc21b9232dd80bd5eed08bd8dd6" + }, + "providers/__init__.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8fa961e52dc94a2923538ca3e639c04b01f1ffc955fcd8fe301e136ead81eadf" + }, + "providers/aihubmix.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "a061e555cab9b86da52910ee53d8bfe229a91f517f3617aaaa680a17be279b56" + }, + "providers/anthropic.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "28ec570d523c8326c073ad8fc0aa051719e0aaaf2e8a468efe9e1061cdd9c528" + }, + "providers/base.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a99d39fbbe56c002cb86ba0524748c98d1bf10e446118d40ae2bb2792db2e344" + }, + "providers/cline_pass.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7b208a408a4484c969f44681e0be5da2317f5359d664677d18408139f21a9072" + }, + "providers/codex_everywhere.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a1c14b9b39c3e65e3583766d98375031a923d312284b6f33da7f213a0c3d0787" + }, + "providers/gpt_image_moderation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b21ca2efe3c94d39a4e89ddb6a485ad3bc909a4803485ca2c9a7d717b5a46036" + }, + "providers/image_relays.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "ffc5bf871bd44660aacd4b202ade999f99d15bbe5cccb206e0642964b6b96155" + }, + "providers/local_inference.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "d87cb7b407afba0ac15caa8b5dd290134b650fcaffd623cfa9a2e9136ccb30f9" + }, + "providers/nanogpt.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "21c97f8bf80f4003b87498ee999d2c248da031005037df4ce02e40ea1718d9ed" + }, + "providers/navyai.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "09d0016f9eabecaf2c444c9394762b2a5ed7f65a709c3fe2d9624ffbc91459d9" + }, + "providers/openai_compatible.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9864daa6b33894f3cce5600243ac175b06221fb87ff31416ba96be332d7976e8" + }, + "providers/opencode_go.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0986fe1f497fcc01a28585089565a8fa127d212638b06afe197fbbdb90f54a0a" + }, + "providers/protocols.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b546ee027579cca2e9a1ff17cfbfb446bb3d3f0abdae30baa183a03c4c7e055f" + }, + "providers/registry.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f0d3808a1f0696db085f465a629cf329cd9347c0b60785c369006d4c2ba0913d" + }, + "proxy.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "d111384804103ded22cbb014ec1341185d7394721724461a2a3f9a75b9bf392d" + }, + "request_validation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1c91cb634c91161b6e25917c1bd53c65c3b007c192a27b686affa698406056dd" + }, + "route_helpers.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "d0c3c361f9a98d015a82fe1a9a9069de3534911f409b022cb9a9dca50108c60b" + }, + "routes/__init__.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b" + }, + "routes/alerts.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "12c4e91ff8c23beb7b1c092ac70fc2c13806b67e1ba7afd4dcc89babd26570f6" + }, + "routes/auto_routes.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "05eb09d16ff8f001dd48a97a980c55e30777d3ec4294412db2a3b690b9283b1f" + }, + "routes/cascade_deadline.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f0d107c6552541d925817a554563c6ca08543e0d10de53eb15badcc235170f99" + }, + "routes/cascades.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "13b441e4f87b0656bfce994cf7c9d17ed22ed321bea0c604fc07eaa5a0f547fd" + }, + "routes/chat_cache.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6abb810dc1ee1ce4d7a14416c8b1618773d4a1ca3f6e9e77a3df8d970eb9392b" + }, + "routes/config_snapshots.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c2a72bbaf92cffa6c7547c3a51678a0e7e230aa7d8d88f8a2593aba8803e81a0" + }, + "routes/context_pages.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2664df690add3c758ef2e711bac604a2d215f3b3a22cfc123c77bf5731c3b8e7" + }, + "routes/core.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "00e9c4d4373e9950a35a8debb96e714488f7c3ed42bd4d1f0f402d634fbd62f0" + }, + "routes/credits.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "952f5d13da6c66e0838e4d85ad1c9affea5faf318984b1f3eed781dbd453ad26" + }, + "routes/csrf_errors.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "dcc9103be9eca895084d5dc0848fc367bc1c147bbfd973385254216f518c2c16" + }, + "routes/dashboard_chat.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "e4074f254cf32d43ee7de2ebd93f0c590454b246a23d558998c334ce7d28901f" + }, + "routes/dashboard_security.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "38f9c7b9262fa1275c02a7a1c95b25e3b67d8e16744f8573797514ff87be2239" + }, + "routes/documentation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d10850bfcc5715a3e11b9601cd1b3c43cc52fdbfbd0e3e4436d1ba044fd5cdea" + }, + "routes/enterprise_preview.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "309e274dfb03e6a8dc9f7babb98f2fcc0a8951266661209687e9757bd4429448" + }, + "routes/free_response.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "34df91bc3650bb09fbce76589e5d2dacc2c3165fa75ba6bd9d9f7245eb466263" + }, + "routes/free_routes.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "14a413e5b92e94315833fe0fc06ab7fad2fd92568c788d74565b2ac1d607aaba" + }, + "routes/gateway_batches.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a9cc3bc8eb3d25eaab4efefbde57bc6506d4af6f7d4518394c92af861bd4ac77" + }, + "routes/gateway_mcp.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "24d514965b61712a9e027860bd2a2d7aa6f4ba5a5929ba0a9f73a005707192f7" + }, + "routes/image_quality.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7eca100834d2f02c896bd7775a170b805893a9d95f87752bf501fe2247b9bb11" + }, + "routes/intelligence.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6bb99b0b757b5b25e90697415d1e41e7129dc7a7ca227915935afad56c7556d2" + }, + "routes/intelligence_media.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "40072ff1b7b45b9216a85cc17625206a7255f329693fa8f581b744a700974090" + }, + "routes/knowledge.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "3bc186b4f812dc93c17756f4be0160c9d98d816956ade81f5bfb11161739fcfb" + }, + "routes/knowledge_alexandria.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b5cb8fe752664eebff7308de55c3283c2bb91c838c9a828f294d525d29a1d259" + }, + "routes/knowledge_handoffs.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7b67f7c3da4c963dff6388fabfa4ba93b477404eefa846dcd9c341ffb0b73da8" + }, + "routes/knowledge_management.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "55b14a47d162c272722c07082d1a263184cea73d8b7ff102b556c53f2ef675b8" + }, + "routes/knowledge_mcp.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "488818e6f67a4ab50547588fd1371b30b587f79e3cafd3e28d145bb8e38cc47b" + }, + "routes/knowledge_memos.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "de86c533b80a75d25fd5f3a77763e957a4308b39d2de33b381954c196eb5a4a9" + }, + "routes/knowledge_onboarding.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "5adf66ce4d4d733ef8ec551b2b92d0493164381c5537a3eab679b3e3c24f55b6" + }, + "routes/knowledge_skills.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0f378fdaf735e7794496b4bc57d1d7815755cdce857936f60f84674a822496d7" + }, + "routes/media.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "14e989ce1fb678535043b5a08139f37f1e10bf98c8c11ab360550bb22fd0c37e" + }, + "routes/media_audio.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "ac1e69377343bdc41f19d66b756213571e6430bd28274146e9e25c2d60a11170" + }, + "routes/media_batches.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c58b066a6b37681212e6b8b55be8b01400fd64a337d54546b3002e1c33d7e32a" + }, + "routes/media_edits.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "30ed0f0f18529df52f06f1fcee0a2186f75c9376915d0759a6b036df46455149" + }, + "routes/media_files.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "67aa19f99f3c7fddb4f5f2c282f1712d29c745748b3e3078eaff98b8a6c642bf" + }, + "routes/media_images.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "be9e194c9b2ad34cde76fb8367a735cc5d71f8b6141278fd02124840f26a3697" + }, + "routes/media_narration.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "ddef74e625989431fd9ddea741cb8315bc8b96db82e011d338c79b13668a8879" + }, + "routes/model_discovery.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6ee413d25736340b784e1d718a3c50e5d7c1a483a2f4228a3fef5beccdbeddda" + }, + "routes/moderations.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "11e5b53d8a37808f53adb1f8b8c0ea9d32ded688c74cebd4abf8e8ca115a729d" + }, + "routes/optimized.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d4c565cabfb124acbbf2253e5e1e7337fe889425dc8d486d75cef8ce24746e72" + }, + "routes/payments.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f7fd931e0bb1b807ee8182b54eb5b91f50866cdd0f47445020e4159eab4357b9" + }, + "routes/prompt_templates.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ef3c1e67b43b98e64105ebc79e38f1a12fc245d6a8998db27f16c69072127343" + }, + "routes/protocol_bridge.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "afbb07e2876724ae67bf4f665b6651af29da6ee4d732d9dec031a17a622ef92d" + }, + "routes/provider_credentials.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "7ed444a62db36f68a4bffa9f4e74c762212114989c8a00efc9fbb0c2f059d1a4" + }, + "routes/proxy.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "265e183e46865675ea3963f49546f650b3d0a04f98742f2dec63c7da862868a7" + }, + "routes/public_pages.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "121a7c8c51b8d8a290fefbffd2a31d392385c13694759d94c39e19d73b98c285" + }, + "routes/responses_compat.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5c3cb763f26dab741a57699fa20ccc1e5435057ddff852669773ee8318ffff64" + }, + "routes/responses_state.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "195d37cf285625fdde72dafd0d148325337dc46afdc6e683accd4b9e8b90f7cd" + }, + "routes/safety.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b650a8ef7a81674b384eb192fa0a7d610ddafe948eabb241d9a88c4e4bd1ce9e" + }, + "routes/saml_federation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8a8c12d9da440382a5479a3cdea6fdb15a1f6c67e92db1c4db0b37ad84098a34" + }, + "routes/scim.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "20a69ef7eeb656377e73cd550ff2b247c970e49f72bd84b82654b33b4454bcce" + }, + "routes/shadow_eval.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3c53c36a2869d5e3ad7093a7b95819ef04570f222f321660c45a35c459c64724" + }, + "routes/status_page.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3e460f0ac27a7392b2552905a73211e13d5d71e7100cf5cf4193646eb3a72114" + }, + "routes/tenants.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e395d65ae1f590baa76bbd09e0eb273bcb7fd83aa2e1e66e3e30b5fe3cc89ca5" + }, + "routes/tool_repair.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "71f5d11cd649208d60b1508c6802760aaed18d2882d7ba4da2af832e9a89ebf9" + }, + "routes/unified.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "bcc073d8fc97c65c2b00b1d7b069284ddfcd1834ab610c640e0d1644a439415a" + }, + "routes/unified_bridge.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "15b84736a4e28cb594e41b0495c1c064182229c76e9aa6258392579c65b69999" + }, + "routes/unified_images.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f5de8bfacfb4d754f9c8e5d26f0833b6cc28de39aa3112aed94467f63942f550" + }, + "routes/unified_messages.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e0f1a70c70d2f1a0020448978d366e4770b260e5aa662ece18d9d4d7a5dfcf81" + }, + "routes/unified_transport.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cae7cf3b9ee2e74f0fec5cab3c55d71fa71db63facdc5e98f275fde4f3cebe87" + }, + "routes/usage.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c222884e9bfda52a2db4b99ed88c49de4be5d27496e8806758a5c2d34ff9e362" + }, + "routes/usage_receipts.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b4b8e72c0540be4e3c8241834918f3e535b36b0dea82c9d6da17df3a56ab02ac" + }, + "routes/workbench.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0f5b2cf70441ada654990182353035db0bf8a1ae4af77abe87c2016e6860b9c8" + }, + "security_config.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "26d999fe08561d26d7cf69a46656f575bf22d7d357fb9e4eee37c03a0211f7f7" + }, + "services/accounted_dispatch.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "647d4233ca8d7927b71ee6c74aa414914cee1864bd979b4229741ab67d66bc35" + }, + "services/adaptive_context_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c9b44d71b9d67d298c5c727b7e2f48fd10688c3f673cd181960ff40ce822d43d" + }, + "services/admission_leases.py": { + "kind": "internal", + "reason": "Private admission lease RPC.", + "sha256": "b6f26de1c81f38615f585b55d3585d2aa12ad74452b1677266af3a99979852ae" + }, + "services/admission_request.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b003b9c3bebaa1234b03ee956ddd21baf1a9f255f9d73d3278c96dcf71811443" + }, + "services/agent_setup_prompt.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e6dc38246069e30b9cf5b14cd2bba10bc73846049be5ccf6024086a5e761befd" + }, + "services/anthropic_catalog.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "818c1e657d2519390dd77843a483a64fc0252083a8e5572e94721a3898e8b6b9" + }, + "services/audio_codec.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fa56ff06817eefe0cc33f4c87a6f85c52365129c724a8d331afcfb7729cabcf7" + }, + "services/audit_log.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7b841926ed623f0c94c8fe864b3f25b1a6a5cbd222e77ad8c9788d05fc778901" + }, + "services/auth_primitives.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0236bdad078e24d37db938cabe1db820094f05605f7ebd87c89597b0232143b8" + }, + "services/auth_service.py": { + "kind": "non-provider", + "reason": "OAuth credential refresh authenticates the operator; provider generation is separately admitted.", + "sha256": "4b05ae698b86c28a9f1c07f1e4c447b00ae75c7c8ad7e6273984a1929d110150" + }, + "services/auto_route_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f944b899963d1cb63c339dc36ebe5c7f20e139106964a86a4dab3b63a6ac8355" + }, + "services/auto_route_service.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "8837d1dd009fc9bb8c16da383caa370cf3f89933cff9ec46b592448709bdb8d1" + }, + "services/bandit_recommendations.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2b0b86ba442b0fb9c5e9f1bfa8c27a84b6560d27bbf1bab9f796d9067c4cb43e" + }, + "services/budget_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2bce627b9d973dc4fa989d1aff7e2226dea6cace8592d89485c49a8ff4066eed" + }, + "services/cache_policy.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "8d905ec9f2da11ed397f08aed091091057adbab9082f8668354c7eef91c47fab" + }, + "services/cache_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b6e11421a1ec86a8e6b99f63a2ea71011a0cbe79b9773555f6839c527f581f0f" + }, + "services/canary_traffic.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ab5e3f57b31e8afd6b1a3f2032094896cfd74ef9dd5381b092ef99f0dcad8dc5" + }, + "services/capability_probes.py": { + "kind": "non-provider", + "reason": "Standalone diagnostic client calls the authenticated gateway; it cannot dispatch directly to a provider.", + "sha256": "da5c7c9e93413dfbd26b63c3871e79a0d35f5cf046e5a0b10ef9395578ead3b0" + }, + "services/cascade_checks.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8eeb6a3cb749dd566d35dcbe8418ccdd9c66f4b9dd0d24f59587b0e4d6b4b043" + }, + "services/cascade_config.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "553bdfcb6eb61f20045be3dc245594a403a993fa45d9f45a139b23e582dc9bae" + }, + "services/cascade_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c232e46e097c2f36eb6eba36264070b330eb3a889ead2458738934e766b20b8e" + }, + "services/cascade_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "201e349af7de489aa26014f316ccb98d853694eb0604cd99b478245635ba8b7c" + }, + "services/child_safety.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4ab665ab1feed38228d8ad05422fbf7efcaef914042381d2d24caa2fce36c05c" + }, + "services/child_safety_inputs.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e9ed64edc3ae20dc3e96dd5bea93850681b11ab9f70e50bf79c949243d231766" + }, + "services/child_safety_settings.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cf0efe175e2cc70cc5d1e1bb9c0e075ed084b41af648eb95e2f62722b9eb2cf6" + }, + "services/client_headers.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d7b07da139924e4153869b51e43f4bd2a6e9e9a8ae64ddccce1631fd4ae7b704" + }, + "services/cloudflare_ai.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "1d7a76d1da81202063b624e8d6058c0191b6c7f7155b0d7db26f8e016eecaedd" + }, + "services/comparison_lab.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ea61b938ed812b1979e397b3c641bddc0ed91a0eecc1d47484633554fa4cf047" + }, + "services/config_revision_sync.py": { + "kind": "internal", + "reason": "Private durable configuration reads, never a provider transport.", + "sha256": "489a9573ba8c5c6de36af1b7d2fb53a687caf91b909584a9fc944f266c44c4c0" + }, + "services/config_snapshots.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "da52ed20947b939cd0b55d6759ce6d64d32011b6217e4ed3b1817e806fe4e051" + }, + "services/connection_profiles.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a30c7f4b48f8f0db2ea99fc7c5f95c32dc9acc1da1ceb4ca3bb42d19738a27ef" + }, + "services/context_analysis_cache.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "bfe23758188a1d40d720d4a4221b8da80f9776440f807cde1a1fb4bbe31fb84d" + }, + "services/context_canary.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "23ff3a3a73fde62f7ea835a05e9091ed4f8602d759ad1e252dc8552d8a8fb56c" + }, + "services/context_canary_hook.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "acb6e06daae2bed2773f93232f549517feb117369d2269f0617f14615ed2518b" + }, + "services/context_optimizer.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "152fc1433e2ff265bcbf40d5699089af1526ee53339d4fbeac11afb1180a5d00" + }, + "services/context_pages.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e6d95d1590ef50f525cda666c1be3782a9f5fd92d54a403ab371e93581dd3770" + }, + "services/control_plane_backup.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6ef334a76f33c90292c2857fd170b8c2d5fc1abbdcb61313edce23381cda5943" + }, + "services/control_state_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "33045f2a165f333094c34e9985e77851aa94d4e0293ea0915d01a8f5500007fb" + }, + "services/conversion_diagnostics.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "de04c864969fc9b5b2a283775907562fb8aef80d9b425165249834967efe854a" + }, + "services/cost_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2c51cf7c6558ea04123eb94361b942aa8958e465a76fceac06800ace1f09f4fd" + }, + "services/credential_context.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "d305faa4f30c9400f243639e0d775169c9f3adc4bef93669c517cedb6a331bdc" + }, + "services/credential_pool.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "aa6ca6ddfb0137452cc53ecff300d13f383c39836a3005a2a56e7481d6e90771" + }, + "services/credits_admission.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2231471cfd1042e186d3e9af7c6f93fe0ab38dc363296acf977b24f82f55c2bb" + }, + "services/credits_ledger.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "18d1033bc5c4fbe76f027fa818c0a06ff4257a8d58e6d80a4cab046419177c39" + }, + "services/credits_payments.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d128d9cedca99ff187d064ccd88d70e4b0db1cef2f0e1867a45f83c3e0d8d0b6" + }, + "services/dashboard_sso.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "500ebc3e06213f94cd0bef8594709b6ce5ef50c6bb9a6082144033973549c61e" + }, + "services/deferred_tools.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b2a006acdaa5903e197610863f9576438e0a05f233986b9bc4bf1644a9270626" + }, + "services/dispatch_network.py": { + "kind": "guarded", + "reason": "Origin-bound credential and network capability boundary, with current admission for each asset redirect.", + "sha256": "3322243fe6fe13f5e8d1cfaf666a806fe97a95f9b6737722230f478f782004a9" + }, + "services/dispatch_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9ef501fd08282710e37a3c6c4f43a790c0472a9dbc925bd88680fe689f54d68b" + }, + "services/enterprise_contract.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5ad07545aad25a83b2ff33757555a6cc05b61c8f6ea917ab80fa1cbc365998ee" + }, + "services/evaluation_noise_floor.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a15da85e3639e4a6ba061a919bd55dbd76719894c8ff8c8520c944c96ffcf198" + }, + "services/free_compatibility.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fc0d0eea4d8d63c59334c3686f00535ee9764eb3c8881fbbd10732b20c663a36" + }, + "services/free_json_contract.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8bcd19b617b94cf006ba8a8d498c84b0fb17050b4af1df418d67e63d30a00cd0" + }, + "services/free_model_policy.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "0d0e2b03a2c68076b936e76e45498c2ff6365711019da0c6103722c8e89dc587" + }, + "services/free_provider_catalog.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "729133dcf51aa218ec583cb5bea3dc80cb23287296ae45fddd1d71103164f34a" + }, + "services/free_quota_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "217b385ac0a82541acf4bb08d3cb1dabc6c0ababd1cd46ffc86a211f8990e74d" + }, + "services/free_quota_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c376f7cca1e95771f873a9176989fe6b7aa6d3abc01ef6fb07bf8bdd3e9e5e9d" + }, + "services/free_route_diagnostics.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a7e9e2746f3c656edc203acf4404a131918b69a8a40fd75571b38a467d8fd33e" + }, + "services/free_tool_contract.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fb689785e7ed1695c121d5bfafa50f53bd1aa42cc1f7a28c2b8ace91c34a37b6" + }, + "services/gateway_alerts.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "48dff8d277ed73105cdf912138e4dad6bb9fb750ad0b21e1b4ca5c8653344559" + }, + "services/gateway_batches.py": { + "kind": "internal", + "reason": "Private batch persistence RPC; managed execution reenters guarded Container.", + "sha256": "7fbccd11aa42b4864b32a745377fd067ea281aab0ff2711e7934c4f2e37ba854" + }, + "services/gateway_extensions.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6cbdd3e49c37f99c3d2c83d110db0bbc5c503dc495fcaf03577f655ac7e70cd5" + }, + "services/gemini_audio.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "cb701602a93793381683c4d73cf3cf8073e7962f7380ddc0ab5a14116720e4ac" + }, + "services/gemini_credentials.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "f8458ed0a5d9c02741d8b02fe16ab3c43d7d17bcc126f0e3abfced046f84f038" + }, + "services/generation_deadline.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8c25159414aa80291d0d9794bdfa86a05c90d962d501ff81705063c21cd03cf5" + }, + "services/health_checks.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "93cf31cb1115a10b66d51a0c3784db7d20cf0c7132b15ff39bb583c37a3fae28" + }, + "services/hedged_requests.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6336d87f0ca70c0a17b252ec0eb2e93e47e3ac6031153e8d5df8cefb06a4aeb6" + }, + "services/idempotency_store.py": { + "kind": "internal", + "reason": "Private idempotency persistence RPC.", + "sha256": "8c88d77785a9d3180c83e564aff9feec540cd959b856bcbda17078b4cf809c41" + }, + "services/image_quality.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "49f210a603bcd4b63160b3a6128e0b1a91cd9e9649da982dddeed55d48ac5714" + }, + "services/image_relay_catalog.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3ba2d17ed1234d08964fe1fd1e57a6e7d6e19f9ba7b9211f7a1e345c966b4f27" + }, + "services/intelligence_anthropic.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "92776b86633baddf56345bed0b49c7d53f03cd3b01f8d7e98f89271c5065f240" + }, + "services/intelligence_auth.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e512eb322e5e71e1a520a1db348a9e397d84f9957441ac335c4c9e9d28b9ad31" + }, + "services/intelligence_cancellation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f197a9276f0281c17995fc5411a8dfff16e2a06dc960af9da5a9db7d891fb43d" + }, + "services/intelligence_contract.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "be470d53857d00571267c8537f58571f7e93de014d9c8b6cbb369b40f31db3b1" + }, + "services/intelligence_d1_store.py": { + "kind": "internal", + "reason": "Private durable account and policy authority RPC; required before admission.", + "sha256": "f6964a63f5cb996322ffedca041c9fac8a4b6cdeec2f96a69206fb930551b9e8" + }, + "services/intelligence_gateway.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "efac340e944c0444222a44a484c1abf2caf219458b4eaa5ed0f093ac143d0c91" + }, + "services/intelligence_output.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e7c8b68e4adaa2b3a87e8cf7ceb85843b4b021beb095d395bdfec689dbcdb096" + }, + "services/intelligence_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ea72592b6127f19c04411e8eb52eb5c1891c1c152e34b852c50d195472a8f2e3" + }, + "services/intelligence_route_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d5f696a77cbd6388bae9a4e1b4b185fcacf69004c8c9fdeb3002a021c7aa33d3" + }, + "services/intelligence_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f925c22ed7bc51bcced0597f8c769072125d326047b02893c326fd8f7ea70590" + }, + "services/intelligence_stream.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7e812a2528b55ee943b4beef6cb4211894220eef368e4b6bb4316d741f4d520b" + }, + "services/intelligence_tool_repair.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d76e8aa07901d1e3846f07d2cbcab94e151661aa5afa5f12e59d1eeb5f954d50" + }, + "services/intelligence_transport.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2c3e234e66159f49d9cb55fcf586f5034d6adab9a169d564ac10d66372defd66" + }, + "services/judge_routing.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f9e3ca391cb5e3172560cc7c32255f1f3a8f4d2e507bf0354997054846db0955" + }, + "services/key_controls.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3d4b04334ba19c75a2921ebb93323a826e2c3d745e0bb3141f3d55108be06d25" + }, + "services/key_data_guards.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "355a6183654ad51ed9bbe2b62b8901679e3680c5cb0aa5ff7f9bf08aec158b1a" + }, + "services/knowledge_client.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "915e9f2a8ae157ad0d87b08073f6198a70f3b00df778d00bc9d06fd943a7bf8c" + }, + "services/knowledge_native.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b0560510e50edc6935f10c3dd21584a0cf40e0f8f2802c691f8b5041c7e8f3b6" + }, + "services/latency_slo.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c529c365464c4b66995c8339add1eb03dbc3f5b2dd8dde6e9ff59d284179b1b1" + }, + "services/latency_slo_candidates.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6cc84ea11dd2a034cda5a0735af7a08261a140144895ba535e8a14171aadf197" + }, + "services/learned_cooldown.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3fb690e621ef2dd9a3a554f8bd6459f45631f16ea34be61f7a8b81e92e1c7c53" + }, + "services/login_attempt_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "784600f2fd7e0108456baf6a534b951049e0e2f05a202283811f51ebd4a17276" + }, + "services/login_attempt_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8a2f75ada5cd6e377ce7419ec38d4595777fa7d42eea9f6c2562210c1a722c7e" + }, + "services/managed_dispatch.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "3c9967511d53066f740053ae04c8cfc123fdc44f77ca52e96fb68e40aedffb7e" + }, + "services/managed_turn.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "c619b5fd489667332193b797375f43eae84700cef42dd779b88bacac8f6c7f55" + }, + "services/mcp_contract_drift.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7b000cf59b897ed7ca96b246065c7b90f7aa62a13914c30c2b661d996fa0696c" + }, + "services/media_catalog.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "f0f12c531156ffc99becb9d607eb0efb18473e1ae58e0562f38333aa85b064f4" + }, + "services/media_jobs.py": { + "kind": "internal", + "reason": "Private job persistence and scheduling RPC; execution reenters guarded Container.", + "sha256": "b47be5492e36a219cd743b5dfb8c9467e172c678be588600495d5e1ab5118f13" + }, + "services/media_signing.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cbea9c4db4aed6aabb7d46faf402568f511ee03c2aa4ba173e421d543f052f08" + }, + "services/media_storage.py": { + "kind": "guarded", + "reason": "External downloads/imports use provider_http; remaining methods manage private R2 objects.", + "sha256": "c7eafb0cd39349ff767b8fe1365f3651b4e616608005e72c21dd1def8929440e" + }, + "services/media_urls.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "27deaeb534d23e3622377742318173ead0b9f1a883a4c00f3a6be44cbddd8b78" + }, + "services/metrics_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d8894064ab2e8add6184c06c2ccf9303dc583ec9eecf0c0f7c5877fbdf58d880" + }, + "services/model_catalog_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0fde859ed00e534a90c85891d8f182af6c7934c51e7c2c8c19a8dcd0be6a867b" + }, + "services/model_cooldown.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "57e22aebb9fe3b7fc6e59715a7d3a667bae8ae3dfb512192af70bd371331cecb" + }, + "services/model_override_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4df825212824d678e571da03983c5e3efba62b317e8cb0d0c382ab68b8708146" + }, + "services/model_precision.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6cf35af44a625de049e0610e276114f4e2df35e44147363475ed74422a0c8815" + }, + "services/model_registry.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a112631d7a4aecc391fe5f0951ab01d450c9690dc274c1f2d075e44b16f11847" + }, + "services/moderation_input.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "79d9e46bb2085859845153a084cbb912c2665222d2d1f78e2b5118dc0cc474de" + }, + "services/nanogpt_key_pool.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "78d38e691062bc61dc94d0d4d5bf2ad64025d410b3429e63daffc418b6de2a1d" + }, + "services/nanogpt_speed_breaker.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2c0015cb90af00adbd074fd85158566afde15e8e142472f4321f0b04ab95f15d" + }, + "services/native_token_count.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "46cd0e4dff7d3d123fe710c1cc00dcdf39a57a56ce1008446c94c8f4975282ac" + }, + "services/observability_adapters.py": { + "kind": "non-provider", + "reason": "Explicit observability sink, not a provider generation transport.", + "sha256": "1bbeb8bb3c74a42e996dc6c57c489203451c0b73f7f8ef31c2ffbf53981e5676" + }, + "services/openai_safety.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8614fe5f305e4fa322869f5adfb1fbee6da6dc4eae72a98646577a68b487ab44" + }, + "services/openapi_spec.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4aadeecb28e6b81deb0f65b2e720782a6d2b02fc5137eb7a4bc71a719c488929" + }, + "services/opencode_session.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "64a4b6f6458077825724549e07fff5709ff0d45bade57ca376abef0296bdcede" + }, + "services/output_schema_validation.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "03eb2a5a4887153c11001fbb107928031097470f6ee62a2704429a5ff2bae94c" + }, + "services/payment_billing.py": { + "kind": "non-provider", + "reason": "Payment checkout control API, not inference.", + "sha256": "b8ed7c0d67fb2d1d0341fbc56cef18e03c92098ad9abd83b2b53a5322f8c5da8" + }, + "services/pii_redaction.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "683ca6757e9d77f3032a4bb6507d7d026588ba20bcdc955d0be2c00981c19e0d" + }, + "services/pii_stream.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ced61ac817e74cd035de03ae60e46889f7b8683398251aebcff88c2c45a9181d" + }, + "services/pool_reset_schedule.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7e76d2ca84c5f9e9934cca7a6fcf70fb89913e93cc65c0c7f347287356bae450" + }, + "services/postgres_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "455a1428798977d982b96f8d9a7cb4d66c80f2ca19f9e4c4d6f0e0dcc09d64a1" + }, + "services/probe_cost_forecast.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "15e5b3b8b896ea63faf2ca434734731e73f5ccf4bc7d45032a764d4e412600f1" + }, + "services/prometheus_export.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e6463d21307cae24a620756492c5ad93907fe6642a2a0b1463f1ed75f1dc9b24" + }, + "services/prompt_cache_affinity.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0b6ba053a2c58e8fa0fbbe50a3315042b371da126de935f58ff0d0fc0519cb1d" + }, + "services/prompt_cache_cost.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "53ed7f57d5ed2ee3e13762f5686414b6a6e6f4b3a0d3f6c53f8ae1d37af274af" + }, + "services/prompt_injection_detection.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5ccf79f9fffdc7d5877ffd36733fbfcaf9be9a67433481f3155b6ceb751c7633" + }, + "services/prompt_templates.py": { + "kind": "internal", + "reason": "Private prompt-template persistence RPC.", + "sha256": "9e785abea29bb7abb88162f146d5eafa9d02fe57b23dd8bf1ece71a957f26bd9" + }, + "services/protocol_extras.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7906366ac64f811f0bdf654312bf97ee4304da0f8365a3ff3557e84b9beaf2c1" + }, + "services/protocol_translation/__init__.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "376ae90cf48be50da1c0f774262b8a9ff09b3d4ec4f3b759858057c3868f2386" + }, + "services/protocol_translation/anthropic.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cff4c368369114275cf11f830cc3b53391c64a4c3e58a06b628e5156ee82133c" + }, + "services/protocol_translation/anthropic_continuation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "876a709fcf9661f9508d6c6e566362901c5133880a80b46c98ac67b678dff7bb" + }, + "services/protocol_translation/anthropic_request_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "61bb336865809714a61f7af0cc620ee618ab805f441744697e8c5758d6d0e062" + }, + "services/protocol_translation/anthropic_stream.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b2709c2e9b6d1abd6996ef8ec9be59ab6cb1cbc291f3f4002c419edfa5c916f0" + }, + "services/protocol_translation/anthropic_usage.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "965da99f1fd40f9c2526d6113a0c213a445d13a580c2faa49b6a52133c71a375" + }, + "services/protocol_translation/common.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8e7824816c68a0ac6c5a9d0d0db7ee56561896f967e6ab8638e2f4ff546f9483" + }, + "services/protocol_translation/responses.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "340a027eb1cd6778b557f9596f49e74efa7a507c2a8a296d573c299260f5fd0d" + }, + "services/protocol_translation/responses_requests.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f1af21d6e83d3aa9dff063fd28a3a2ba17cf02ac4da813d3f0c617c92e1e48a3" + }, + "services/provider_access_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "14a0ea1030daadc8c1c1992b2abdf90ee7c1746b649bb1d4ebad8430ea0fe606" + }, + "services/provider_assets.py": { + "kind": "guarded", + "reason": "Origin-bound credential and network capability boundary, with current admission for each asset redirect.", + "sha256": "7becf18c378881506f2a20e1eebeb0b00f1be38786730eb41268554c93f5ab80" + }, + "services/provider_authenticity.py": { + "kind": "non-provider", + "reason": "Standalone operator diagnostic client, never called by the generation runtime; the target gateway authenticates its caller.", + "sha256": "28422fd44abbbd29cfd0a0fe10900945db87caff4b2022b7731592598444e389" + }, + "services/provider_capability_discovery.py": { + "kind": "non-provider", + "reason": "Fixed public catalog metadata GET without account headers or user content.", + "sha256": "da1ab5cf7982f600d22afdc125dfdddd92e6b1f943a927ff14e84f825aad385c" + }, + "services/provider_catalog_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "651d31ee1bdddb4dc5fe430a82a4c0cf1623ecaa72edeeb008e3cd2ebf9cfddd" + }, + "services/provider_catalog_metadata.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c507e4d54a104fdeb3a06e211ab2ff4191424a764f14b09342dc134759dd6005" + }, + "services/provider_catalog_refresh.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "007a2ff0c9f7498a276c146efd8fab9feca00d16c6d9bd429936e0d8834651a2" + }, + "services/provider_catalog_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b1448a93e4602d1def01aecd20c3c3c9f00475e683d55d74effc56e2db495d01" + }, + "services/provider_credentials.py": { + "kind": "guarded", + "reason": "Origin-bound credential and network capability boundary, with current admission for each asset redirect.", + "sha256": "babc974be1d465cf350e9f5d67f0e692bdf9733f62d475832e78e5f0d106a769" + }, + "services/provider_dispatch.py": { + "kind": "guarded", + "reason": "Sole Flask provider HTTP boundary; fresh durable policy precedes requests.request or injected session.", + "sha256": "a510682dc221b452bc00361e4c5af5ac6bedde49748b5e9e25cef0c2cef0394a" + }, + "services/provider_prompt_cache.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "537ae92b1ede73ae20f5b7a03353e5cdd6a3d7a1ce4c930dcada1465969e3c9f" + }, + "services/provider_request_headers.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d59b8e720d732d1d1934361afc82130453cff502e67317aa67d66be29c694205" + }, + "services/provider_usage_normalizers.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6b65f6f8367b47be6c882173820e4d8bc53936a3f2928dd4a6058e2916691c50" + }, + "services/provider_usage_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "86e844a9e00cd2fc3609131f527b60f853f0d868f9bcd12bfb3f9956e053fbb6" + }, + "services/proxy_documentation_service.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "66bb86aed9afbe5ad5ad4ae0cdf409a858804ae98a4fa0f0db261b7da269b413" + }, + "services/proxy_service.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "e4241056d6a511a101367ec27d46e6147ffd62f86a4feb91f6a133954f5ca505" + }, + "services/rate_limit_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e9872676cc6bd9ea8ec9741c2b8eaf42d78e007394d898071c34ab3bd6e2491f" + }, + "services/rate_limit_service.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "cc2fa4360ea9227e383ca90871cd352083ca00366058b3e998007ea8b4f10193" + }, + "services/reasoning_policy.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "9ba417687b433147fe16b97238d94398d60bb782119536414aecc992fe108eec" + }, + "services/redaction.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9b8bb2d47102cb59e278f1b3c986451b808be3d504e2256f332bcf9a470eeedf" + }, + "services/release_status.py": { + "kind": "non-provider", + "reason": "Operator release and health metadata, not generation.", + "sha256": "1ec73290f38b8896c0bbf179be435d5e55256cb257b97ea3bdf751e063db6ac8" + }, + "services/request_accounting.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "da61afcdb7e66f1f43a7f9c315fab0de50440c2d0760b90d78a555bbe3b569dc" + }, + "services/request_cancellation.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "efd3bb06aec60e0fc0621ad2e481dd7db5cf03bfbf613a061f8e3feb7f70f723" + }, + "services/reservation_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fc98e355d1d52354219b05594b39d0f89412af753ab785debbf263124c975f63" + }, + "services/resilience_service.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "591efe8a9a33ed172866bed6960a4d84122563bde85bef297d62614534604d8f" + }, + "services/responses_state.py": { + "kind": "internal", + "reason": "Private Responses state persistence RPC.", + "sha256": "5f247c4f39f97bdf54eba1e91a4090a4d147d0a0501074be6f697b78a001895e" + }, + "services/retention_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6de7def22c49e5e5017bc028bbc4340e8c84ade1870c6000c59a767a3443f953" + }, + "services/retry_advice.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "09fa87b2d572be6d1a800a2aeceb6c650243a834a4cc10184b72f4dfdda28e3c" + }, + "services/route_health.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "50d9c2477fa6c5f74a919d65bbaf45f4913e7e2311584dad4ce8529c0858ba09" + }, + "services/route_health_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0f9ad892a772b8fa2302fd78de68bce4d9712562009b9aead1b0d84fba1bad27" + }, + "services/route_health_sync.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0e01629b69c639a80380c9835540ecfb68c5b5ad4ce2cdec9e9cfb5afb0bb91b" + }, + "services/saml_federation.py": { + "kind": "non-provider", + "reason": "SAML metadata and URL handling, not provider dispatch.", + "sha256": "5a7c15c202c8b85173cab71392e36f977a77acccbe6a11b38e33ad643d6b10c8" + }, + "services/scim_provisioning.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e7a534b4954db0ca053bea696ff14fe3237c997c1a1d453f7ac9341cbbea3a34" + }, + "services/secret_firewall.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "9ac96591c2fdaa4fb5e880f4a7aab5fa6b9d7604c299bcfba6b5d0de1d5bfbf1" + }, + "services/secret_scan.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "73a9d92bb572ef77a942505146f3ea2110d5dcdf5c2de62969f6fa1942236072" + }, + "services/semantic_generation_cache.py": { + "kind": "internal", + "reason": "Private semantic-cache persistence; embeddings use guarded ProxyService.", + "sha256": "4f1d3bfb79533399a8210b84f9f6411840f36a8073f65aae9f2e06f6f8125d5c" + }, + "services/session_tiers.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "69c717b783b39074f673ff6ca6c92487ea518cf5e7e0e3512de4520c53a36ae5" + }, + "services/shadow_eval_context.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e78eeb8c12885e2ee56d05d86aebbd87d665ddd145b4fd6d98821e205a72b8c8" + }, + "services/shadow_eval_contract.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e435ee51c9f902577a830c54490208e4a039e04151b1862d104c579d18129ba6" + }, + "services/shadow_eval_data_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "315cabecae855804deaeb033e1b0b3c9517306dc216acd6f51c03d1078bba3a2" + }, + "services/shadow_eval_judge.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f88c2c49d9b5e6a0efbcf19ffd5e48a81e73993dc284efd177676bbd044df13f" + }, + "services/shadow_eval_league.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ebc2b5432ba341909a1adb106bbec44306ad88a1790698ee6f979cf00b9b0563" + }, + "services/shadow_eval_runner.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a6b6c7c27115c8fd7b318b2c2c9bc43df6af818217cc7e389a77bf1c4351f3c2" + }, + "services/shadow_eval_sampling.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a6bfb6d8b1219e027b4d6dcd27a27e046cb6447bd4982f51ed503b16c313cbe4" + }, + "services/shadow_eval_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "71c3664eb647f73cea18952c956851819877d9cb87e8923b9ec3e6fc68e4fda3" + }, + "services/shared_generation_cache.py": { + "kind": "internal", + "reason": "Private cache persistence RPC.", + "sha256": "58ab29b11f7783626ae5b37438fe60f888b2bab03b1dbf771db7fab2fc24052c" + }, + "services/sqlite_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "58c4a99a51395ee084aaec97a63df8b7ca76d09262a031ca75c437323dff2859" + }, + "services/status_snapshot.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "346b8abd8279302f3defefb5c73475a670ea8bc29e64d80eab55bbc893a12044" + }, + "services/stream_cost_breaker.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7b9b9155a0837671f4d159c694e558def292d3464daa77d26f0a37ad85f13894" + }, + "services/stream_preflight.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "f5afb2b2150c8048e45d9d008042fdf9d1ddd7bc33117681508d0768d067011a" + }, + "services/telemetry_export.py": { + "kind": "non-provider", + "reason": "Explicit telemetry sink, not a provider generation transport.", + "sha256": "07596f7c9628e83ac10a6aba05e61b80fb7792c9706a6a27a5704c5258ad9531" + }, + "services/tenant_governance.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "99bee5a2c5818507bca232e244909d956d6519d124ea2bd9a6a06c43ba6a448b" + }, + "services/tenant_hierarchy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2d3ed64f579d75766679d61fcc3128087fe17e10bc6251fa415797ac67968f14" + }, + "services/tool_argument_parser.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "06c5ec7b9c0f11340b33d94904c04d0fd800241945443faafe7225775e3c4673" + }, + "services/tool_argument_schema.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3b3e806762952725199d34d01334c98c6c4e2095b45a8571f5df7dfe6007bc1d" + }, + "services/tool_call_repair.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9cdff918fa6c59ccec78b7b48fc02564b1e4316e15fe51c0d699ec50b5f74b52" + }, + "services/tool_repair_runtime.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1d2c42df65624c21abf9e8fd3e7f2ac00f779983e0f773b499208b53e919cb39" + }, + "services/tool_repair_stream.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cc695d54de018b74ce2ea335f334951b8c8cc0089a40bd21bd99d0d61f95549e" + }, + "services/transport_policy.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "842411432b5ed2718d857a70c7cfde9137d00004c46434cb4ea186a1cd17e8bd" + }, + "services/upstream_errors.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "69acb80ee747e3ecd87c596c2a581de624915ac119d3f0ab6cffe217547a0ca9" + }, + "services/upstream_outcome.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d164bf54137031072198ee944a537de9d6f4d77c967d9f5083a7c812169316ab" + }, + "services/upstream_transport.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "c27e7589a1efbc44b8bdd546199f087c4ec08c852e811156c6be149f0fa0690c" + }, + "services/usage_ledger.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3f5530de97b8947bcb949eecca11c4905153820d7297d3e683e7cbec92a93313" + }, + "services/usage_receipts.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fc44eb84c4fdfdab36e27c5f747bad9453d8067c60f822d1215c3272ed05f5b6" + }, + "services/usage_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1121631c97b778163533deba4bc523f5c6ae31a837ebb6d9a90cd5dfb21336af" + }, + "services/usage_types.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d7b74bd38f673925fa882dd4bcd3a34bb2f85adb9ae4e488bfce12e5df43fc2b" + }, + "services/user_provisioning.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "046c8ef1b03e93fe768494f4727f356f026b72b5eb550aeeb813e4d451d6f96e" + }, + "services/user_store.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "22283e5ee0d73b43c8ee93e526b88a6e59e167f1b1dbdd5b96ba2069d8a44ef9" + }, + "services/video_generation.py": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "f4b24fcc3509dfddca33a7be8ddb976e05f277b73fe73a4e7e8cbaa4bc2ca770" + }, + "services/workbench_d1.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "74f87a7ad83256f28ae3577456c6c6007b8bf33fb283d129fd22b72dce93301c" + }, + "services/workbench_gateway.py": { + "kind": "internal", + "reason": "Management RPC to the registered Worker; generation reenters guarded transport.", + "sha256": "3b6b3a64aab462fe93bf1952de3c456124bbf4ec73d598c073881d191ac0373d" + }, + "tailwind.config.js": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "75e7d622614663d473b1e97a237d2ddd0fb840195013ac402520adacf41d2464" + }, + "vercel.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7e04625f1b75e45a47d17879bf6d1ee03090403ee4aa499d2a657fe63883f4a7" + }, + "worker/access-sso.mjs": { + "kind": "non-provider", + "reason": "Authentication certificate discovery, not generation.", + "sha256": "1a1535e5b8b04051812e723fa808c7c731fcb24695ad6b282c636f76e5940a1f" + }, + "worker/account-contract.mjs": { + "kind": "internal", + "reason": "Negotiates private account RPC metadata; no provider dispatch.", + "sha256": "6c5a5ad77c5d86af5aadb4ad13882f253c6cd09d6f76d54b2959624478d83c35" + }, + "worker/admission-do.mjs": { + "kind": "internal", + "reason": "Private durable admission RPC.", + "sha256": "e87b485c50391562216055760a6d5102c038d7d00da6ac6bfb894b473c4fe1a9" + }, + "worker/ai-outbound.mjs": { + "kind": "guarded", + "reason": "Inference SDK calls use providerOperation; external result downloads use providerFetch.", + "sha256": "b9bfbce10ceac9e665c270e4a42a54a06aecd709e35d5cde7594cd257668961f" + }, + "worker/alert-delivery.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "34849266d92a9fcd7eaccd323bdb552e0ff52640d69f5b7205c529e962b3ced4" + }, + "worker/api-paths.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "378a091ba1b4f13ce2e75516eaa425a01c1655624970aac5bfd59627e5cf4a54" + }, + "worker/auto-routes-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f250eb0fc1b0d59dccb0877a933e3af6cd6bfbd0f5d4d46e14f772becb6d5003" + }, + "worker/batch-files.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4c89419f46d884f5fe0a34eb437d31ffd51644c1a3e8b56009e1c646a97dde9b" + }, + "worker/batch-jobs.mjs": { + "kind": "internal", + "reason": "Private Container batch dispatch; generation reenters guarded Flask transport.", + "sha256": "20feb9a3ea43d46c7b862709989007eaebb143f8ed4561dfc35a053bf441aca4" + }, + "worker/build-id.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3f187f55a86a64c8b9dff2b84c0828cf3d3e6e7b538b79364c5f72090ebfcf24" + }, + "worker/canary-assignment.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "070d9969f6c5a17fa238c5c4eba2aefceb2cea99bf2c025563f19a7c479718db" + }, + "worker/cascades-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "386648e0c5cd2b24f4d2341f72a4ec01fe869badd630fc1e049b60d140590caa" + }, + "worker/child-safety-settings-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2f8ce0df2b8f9582c8c9605c846f04e8e65bc420ed8742a35926e5d872b1355f" + }, + "worker/client-headers.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0ee194888a0c5f97105110b4811790c68e24333492e03464594e086f47bde9be" + }, + "worker/config-revision.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "616e21f45629f3f1739c365a0c74cadae4a85814396d89dd98459a48da4d1671" + }, + "worker/container-env.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "56f07b1a9d2f6451be990e16038e30d55afbee1dbf5a5e5ea82b30235558d790" + }, + "worker/context-canary.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "db09f569ea16239cc45cae1b379ccc46f52ab3395e098825a607b0dd7d3605b6" + }, + "worker/context-pages-d1.mjs": { + "kind": "internal", + "reason": "Private durable context-page RPC.", + "sha256": "6ccd924a75e5f0cace2a4e6d02b180a08c57e88ec955beb3508f1467dba9f082" + }, + "worker/control-audit-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3c4672f5836f29ffcdb259b8b727ab170bb9b515511999c53fb82be710c62790" + }, + "worker/control-state-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f846d913d2acca4ea289217821a2927aa6fe0cd5aa4654578a67d4ae073b803c" + }, + "worker/control-users-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1ac197ab79dbacd105a4bda811d991e3e4ed1cb7263318601f934c67aedab259" + }, + "worker/cors-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "17ee44d44ae37d6bf63838775c1ce252704e48ba0d32aea3dcb7faa6a52fee9f" + }, + "worker/credits-admission.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a4f5d892df430d60ae4721b9e42fbc4615f000a35fe1d7da391c5dea2c4f2d19" + }, + "worker/credits-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "af2e83b617db442bb9acfccd6a006079addf3bc8f44e860f2626c2bff260e83a" + }, + "worker/d1-schema.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "97848f176dba14a172ea8f0983f9bf09fde185bfefdb9a4ce4bc9603ecfc478b" + }, + "worker/dispatch-network.mjs": { + "kind": "guarded", + "reason": "Origin-bound credential and network capability boundary, with current admission for each asset redirect.", + "sha256": "4df64b633e0800aae5d7228cc49df0980442e2983a87f19d5b6a629f7e4cb709" + }, + "worker/dispatch-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ac63e1f4a06eecdb5eb57a930ec2169bea2ae88cda709aee4198a52e44f4285b" + }, + "worker/enterprise-contract.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "deeaa3e515e758d2d1d84108738a29038f20b07f59e2bdd4fff68320d6b084b4" + }, + "worker/exact-generation-cache.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d137c0f8a5214b51a00421435c33a16b0731de679f2834be6dacc88600b891f9" + }, + "worker/fallback-page.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4abf38c486ce083f1065d8104209c1845cda3cb53a07b09c961e41500379b615" + }, + "worker/gateway-extensions.mjs": { + "kind": "guarded", + "reason": "Wraps the native fetch delegate with providerFetch before middleware, embeddings, retries or fallback.", + "sha256": "51c1e1d303e3113b03f9ec872e40135c08d2a3ccb150eda12079bfad03c5cadf" + }, + "worker/gateway-lifecycle.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b3c2d635a55c6656c27468c914a70dda8601fef341cd72464606128802c25e01" + }, + "worker/generation-cache-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5c7289bd174df4ffb9ea80e63b1f7a1dfad4ec95c305c9edf08ea8b589732f68" + }, + "worker/generation-deadline.mjs": { + "kind": "guarded", + "reason": "Deadline wrapper calls the admitted native transport delegate.", + "sha256": "03b7cbadf4c479c16e619f4a5bd0c8b18f33481b76d4580bb273401368fea8e8" + }, + "worker/gpt-image-moderation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "93a7abc49644407f59b4f3d8a69b24f77b9089d52830eabf9f308c0c3114411f" + }, + "worker/health-schedule.mjs": { + "kind": "non-provider", + "reason": "Private Container health/readiness checks, no provider generation.", + "sha256": "05f0b9921e73ca05fcb5a670494794d2270aab2d1ef975a786ff0ced9b3c99fb" + }, + "worker/idempotency-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a846c6e7216aa2b4eb46614f254c2031a65da5e6666e4485a676414771a4f34e" + }, + "worker/intelligence-auth-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "663e170776310065af597f506356db95fc9a397c670f539e7006b8c081974392" + }, + "worker/intelligence-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b0fc57c85159614e48fa82ec7f76c19cf73ba69f9fdd5a2823b106bcb5a2fc4c" + }, + "worker/intelligence-outbound.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b562bc9345fc1c2570954acc89ebfaafb4c5e056154a9068124a4795c2ba86fa" + }, + "worker/janitor-reasoning-response.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b9b8a35c3f764ebbd2f00a52e3845064457c8d588b0576827e75f4ec2f2c4a54" + }, + "worker/key-data-guards.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "44bfc41aec4f842588b83fef4590ee00f30c3a6f17d4ff122ebd01d6f985a551" + }, + "worker/knowledge-edge.mjs": { + "kind": "internal", + "reason": "Private Knowledge binding; projects authenticated identity for provider admission.", + "sha256": "353927c3ccaa8189ccbee873807067a130e052e44edd3e6a49c96d7361c05764" + }, + "worker/knowledge-outbound.mjs": { + "kind": "internal", + "reason": "Private Knowledge binding; requires versioned identity for provider admission.", + "sha256": "feea69b5b1313c937e423f94dfe2dc32ce1dc6e14596a9fa021f90c3684c4c41" + }, + "worker/knowledge/alexandria/catalogue.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "37e2293923b39027b34c7e4489db73559601cbc5803950f0256abbb1b9c1ab14" + }, + "worker/knowledge/alexandria/contracts.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a9a00338aad163108446ae1bae62840da042540cb50989f5088245fc3fa46576" + }, + "worker/knowledge/alexandria/service.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "9abf7ac28c4dce81e98b393b06b3d0b58cb3ad87e53c6eec81ed812b53d333e3" + }, + "worker/knowledge/artifact-eligibility.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7fd2d2ceac178d84bbdb7e0cea54bf3513cba292d6a9cf9179e38af78e063171" + }, + "worker/knowledge/authority-client.mjs": { + "kind": "internal", + "reason": "Private catalogue persistence RPC.", + "sha256": "d6a1734c1d3e1b6c016db1699dd82b571e9fa8d0113733c22dfdf833a89cb411" + }, + "worker/knowledge/authority.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1cb60b774b4a414ede0954a99323c25be254dc69d64949328fdc5b8371b9101b" + }, + "worker/knowledge/cache.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9380eb3a1e7664bec6edcfed49f47b3b6574dd4208e7afa37452f982bb5a0a5b" + }, + "worker/knowledge/contract-drift.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9193679ab2e6613cbd8446ba1d0b586d2df79467de39ef5d4b0b35b1a56794b3" + }, + "worker/knowledge/contracts.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6a99f8fd80cbe2302f6a1207341cf794994fdbe7a643b87a93fd3bd9ea08e398" + }, + "worker/knowledge/corpus.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "0a1a3fd8ee3d75a5e2e4cd46693de1d746c6f32e4efa3776896a62e8544bef91" + }, + "worker/knowledge/credentials.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f3a20a7b4e82e09e09295a7b3985be45cdffed95226ddd09c42a3b6c61bf9ce5" + }, + "worker/knowledge/deferred-tools.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a31a100364f912eaf926a503e8f32a6d22d85421d896ebc2d5b770ec5b110f19" + }, + "worker/knowledge/evidence.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "894510be603acd8bebeba30b2c01b80cd423206e1ff216a04782c243bd7dcb6d" + }, + "worker/knowledge/handoff-contracts.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9444837e30380e38459f593642a69dc39d31895c10280955219837cfbc5c4c02" + }, + "worker/knowledge/handoff-store.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4e499eb0d18a75ac239dbabebc3c28adc60faef8745700bd5a79e4e092b392fe" + }, + "worker/knowledge/handoffs.mjs": { + "kind": "internal", + "reason": "Private handoff persistence RPC.", + "sha256": "6a5aa961c7e36a989808d52a922efa983971d0e7fdb8025ea4a14580b0e6641a" + }, + "worker/knowledge/index.mjs": { + "kind": "internal", + "reason": "Private durable handlers and Workflow entry points bind identity before provider operations.", + "sha256": "a11fc343b7787904958b12608e5c622e79bf8ce3963e34a481b656c384f6b052" + }, + "worker/knowledge/ingestion.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "966d0f64669bc6e2544b0c09a969c2ec16975916007f35f87c7eaea3ac08ba9c" + }, + "worker/knowledge/ledger.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "65afb28c5a155ac5057552c984ed98217a2343078d3ae0cd297c6bddec379788" + }, + "worker/knowledge/memo-store.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9d725f49f93e218089276cc7acb1584c2b24fc5626681e788c7b3e6b72bf089a" + }, + "worker/knowledge/memo-validation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2bf294d83c3fcb38692b0d539f4ddf67297d0c38d27bc63a5e1bcfc9c11e9d0a" + }, + "worker/knowledge/memos.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "f604ea74e3720db6b4d20a47588a15470ba790b3ccaaffa3509499947c8c9e8a" + }, + "worker/knowledge/native.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "3ae2923e3f94544dfef4da4c21c7c9d101f4aff2ae3cb042a17d138eeb3244a1" + }, + "worker/knowledge/operations.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1039c53b215b926079e995a9f1327dcfd1489420ea1ed208aef239373a4c938f" + }, + "worker/knowledge/policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "70f3bd62a5b33670b0c3f3e9e6a079495942d73e777b495524ec3391734ccc7f" + }, + "worker/knowledge/product-site-identities.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "44c1b51eef62735bfe7df380402898862f7de4faa1234738cb1326db004e466b" + }, + "worker/knowledge/product-sites.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cc964879ca07a669f84fa24d42bc2921d58b0c0decaed543730219ec337e18b2" + }, + "worker/knowledge/provider-site-filter.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1554ea597d9551dc0e767241dd0023a67392c3fba463c67cfb95cdb4a325d18c" + }, + "worker/knowledge/providers/context7.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7973b19f165b17e922e64abf4aaf7ae19db06f2ac77cd7994d4d41720b2c5012" + }, + "worker/knowledge/providers/errors.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8fcc775b14e92f2cf1ecdc870c7d0c32474e1a4f1f935be50289a2c1232b1e3f" + }, + "worker/knowledge/providers/exa.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6abdf22644fb6ccc8be0919711209f34beecf9b6c9bdb5adadf70897a4911812" + }, + "worker/knowledge/providers/firecrawl.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "940b5fc8e3cbbef6b67df416c1d6fe3e8f91786b1b88fa3c4498b0b6d15848b9" + }, + "worker/knowledge/providers/index.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "8bc1f67a0a75c6c3765a03b873dbe5dc768dd5e93388d265937153a89b55d3ee" + }, + "worker/knowledge/providers/keys.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "44ca0feda59869c91b6a52c5c46a67f9b7526106db7de4820dde57d322a181d1" + }, + "worker/knowledge/providers/mcp.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7a3b234117f8f3753e1731f991835a22a5d1c7c72f2ae617caa519e7aec77d8b" + }, + "worker/knowledge/providers/source-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8064a15f5b74b26dd71573b3ad687d468c2ec7a014fd1c5cc06c8ebe9b32c477" + }, + "worker/knowledge/providers/transport.mjs": { + "kind": "guarded", + "reason": "Every provider HTTP call uses providerFetch immediately before its fetch delegate.", + "sha256": "be7f26bfc29523a999ae0fe3f33ee6d9083d5f044ff109b2ede4b7337bb9cc9e" + }, + "worker/knowledge/retrieval.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "aac6df5801182e564d13ed25aae79a202d25a4e430a069f87894fc2c166ba5a1" + }, + "worker/knowledge/service.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "3bb4d263841fa2f3edeaa2f00898b1ba5b5cdd4a955e68443f3f7858eb52d0e7" + }, + "worker/knowledge/skills-import.mjs": { + "kind": "guarded", + "reason": "Every marketplace/source read uses providerFetch immediately before fetch.", + "sha256": "45ecea27eb4bbcc83d459519d6a28579de55acc3e7b9d3cfbb883c6c63c2aefe" + }, + "worker/knowledge/skills-index.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e52215beb724eb3819ee05cdf2a53153506bede5b55949837951e06d4eb71ec4" + }, + "worker/knowledge/skills-ledger.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4d718520cf80bf5f384ddf54de16834cba4f2be9a2bf2caf0f8144407cb976a5" + }, + "worker/knowledge/skills-market.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "c6f6cd1fa18d0caac5415cdf5ae1cdb672fde175285e1f8c8b933ea6c0e8f94a" + }, + "worker/knowledge/skills-store.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "4ae9c433aeff9afe2e9b42c001b7b908254ad510fef3924893c11ef1be237186" + }, + "worker/knowledge/skills-validation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9a9d5f9552297444e4800207a363c47271ce60e6c0f9641eab2a2669c96e660b" + }, + "worker/knowledge/skills.mjs": { + "kind": "internal", + "reason": "Private skills RPC propagates identity; marketplace HTTP uses guarded provider transport.", + "sha256": "0d1ec24d0bd5766c70b77548f72084b48ad70894db575eaee98672a61e3b1de4" + }, + "worker/latency-slo.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "98375b156d17b704ed06dd6c78e227c10d2953fe1537bd0d91b6f7989cc2125c" + }, + "worker/learned-cooldown-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b60fa2f0641f3bf08d9206585527ee4e61e5a6793f2f41cc46443139e3682dde" + }, + "worker/log.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d276aaeff69d0c73f4e935e3bcc8aef2a5f3d045d5e6b57de86fa0704189f637" + }, + "worker/managed-state-dispatch.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "45a6141e259548b450e85375d5f26613d1b6f5efea563dbd890f8b319f5c3f7a" + }, + "worker/media-files.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "eb14b537492aa1cb60cc910d43d5cb87138a999d21b2f970a0eda6e9f789f08a" + }, + "worker/media-jobs.mjs": { + "kind": "non-provider", + "reason": "Private job execution callbacks and configured webhook delivery, not provider HTTP.", + "sha256": "3356956867ed1f3c58fdc4b125aabefc4419d0639422ab347509a8d763e1fa8b" + }, + "worker/media-outbound.mjs": { + "kind": "guarded", + "reason": "External assets use providerFetch; remaining operations manage private R2 objects.", + "sha256": "afa2dbf2607b778c5b736bec68193ac7f902cc8f34f1340a2391b35c36ea23e6" + }, + "worker/media-signing.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8fec18f88b01b3947252489743856ea3c90a3a750455a91f918e34bd8f5cd756" + }, + "worker/media-workflow.mjs": { + "kind": "internal", + "reason": "Private Container job dispatch; generation reenters guarded Flask transport.", + "sha256": "32029d79c9b304b80ed9606a4680fa3517f8d4c856614e396ee70716f88afc30" + }, + "worker/native-config-sync.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ff7115f5d9c88d92274808f9958568d60b7a5584a76771c62ca787048ed57cf9" + }, + "worker/observability-export.mjs": { + "kind": "non-provider", + "reason": "Explicit observability export, no provider generation.", + "sha256": "3c3ac74a0d718362a8fcdef1c7d1f6b0a171efe866796e950c80a2b83f063133" + }, + "worker/opencode-session.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "155251c715ca64be5bd04c3dea5405b9ae04fcda2910dada3a84119f5bb649e7" + }, + "worker/opencode/reasoning-policy.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "ae655230b797ec9589a5172b05249f9cece612e09e539cb0542c1839583cf034" + }, + "worker/opencode/reasoning-request.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "3be9b47cc17a2ca9f70ab03f9228887d4e8245659494b7b6f7dd62c2ccb38994" + }, + "worker/opencode/reasoning-response.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e8c45e0273c5fb2113289c81dae3802347cf9f77cac0b71447b084a86bb4db40" + }, + "worker/payment-webhooks.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9a952e1abf42772625faf313ac16cb13bb87e8aa84e1cbb29214f5b532500659" + }, + "worker/pii-redaction.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "51d45b86ff66a1c26cfa959b097a593fe529406001acea65dde074d2689bfeb6" + }, + "worker/pii-rehydration.mjs": { + "kind": "guarded", + "reason": "Payload transformation wrapper calls the admitted native or roleplay transport delegate.", + "sha256": "919be3b88a68aeaddaba22c63c6e20e19d658964f3cb338dabea6eada0d702ed" + }, + "worker/prometheus.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0726a06b13c968ac8ec13f61f59fd07591e7985e9223660f94afd7ee6eb97d1c" + }, + "worker/prompt-injection-detection.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2a2844dee9774075926ec3ee3d0a67b17c7b531b390e0f5b240403ed1067c936" + }, + "worker/prompt-templates-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "266511dd14adee22d1828fd8c3e70a47bb5cff75268a535dfc64687567aa1e5d" + }, + "worker/provider-dispatch.mjs": { + "kind": "guarded", + "reason": "Sole Worker HTTP/SDK boundary; fresh durable policy precedes fetch or admitted callbacks.", + "sha256": "c6886319a12f270a1ff3e06b4e770d43ffd75e5ea203662dc4d0df99b81b5828" + }, + "worker/realtime-metering.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9ef22465b3c5034a5df87dcb2f2d9d0c209dbe6969cf5d7f4b02c9f88afc88ee" + }, + "worker/realtime-session.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "de5730ce8269079a7cf152e6eba01cdc688b4810d7786e4c7b0b468246fef18e" + }, + "worker/realtime.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "78f30da2e56ec8c552322fd6db11ba3d6d9f7c454cda8771e1db2c76c566e63b" + }, + "worker/request-telemetry.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b8a54faaaf40fdc957a933313b1718e09e25f707d63c2dedf6963a8560a9ae0f" + }, + "worker/reservations-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ba1a1a5bc15fc58bca1c695a22fd0ec22411621e0a093d87c89406a75e410861" + }, + "worker/responses-state-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cbca75eb921b8e029861867b6799df8ef9d2d9a644fe8418da311922f085c7a7" + }, + "worker/retention-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "52a2bf7d5c44953eabcbc6b05519f8c96b51d9be786cca33d2edd7284428d6b1" + }, + "worker/roleplay/candidate-context.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cccfbec402dc653d67d8bd340d3335296ad2bb72264ac8b9a7bea3bbac4a20c9" + }, + "worker/roleplay/capacity.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f8068b510268924b570e0277a097790898b861b75458e9cfa564f419d2d180a4" + }, + "worker/roleplay/checkpoint.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "60c194f4b523fc8fb9a1da5cca1113539bc97061802fcaefd2453088fc9c650d" + }, + "worker/roleplay/compaction-budget.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1e0b66156c1f307d63ec632f055868a2c3b638710cce1930eb92ad9d4d328d13" + }, + "worker/roleplay/compaction-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "34a81069dfed8e393ac2c3e3d0986da3ce4adac980e675676a012df6bfee87c7" + }, + "worker/roleplay/compatibility.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "45ed9b8a1e00b29f0d08de993434b0646aee396ab692cf79e44add3f9666ba99" + }, + "worker/roleplay/completion-result.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "924b5dd596ac8ef89f8bd13f04bc7e1067546ed092e47435e3417b66728f1761" + }, + "worker/roleplay/config.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ea0007c314192ae4718fee801465e8678cf5ec8edd7ffd84e9a70480cc10d1ee" + }, + "worker/roleplay/continuation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0a3abcb3358db0112e4b6e22cf07695947f8bd2d4cde253de033b7f55c29e5b5" + }, + "worker/roleplay/credential-health.mjs": { + "kind": "guarded", + "reason": "Provider credential probes use providerFetch with current durable administrator controls.", + "sha256": "6bb21614ba698b8bb55edef2d1ebb475d7645ac1dbbf5a1269e2c7b6ca3745ca" + }, + "worker/roleplay/directives.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "159d5cb7aeafc3de73f9f03ea0bcf79bd929998b2f6b5ea777a19e3d1ca1edfe" + }, + "worker/roleplay/edge.mjs": { + "kind": "internal", + "reason": "Private session RPC; all generation enters guarded roleplay transport.", + "sha256": "3819cc2b142597537500e10a3426015688124cc2d02291069bd9a4efabc80a33" + }, + "worker/roleplay/endpoint.mjs": { + "kind": "internal", + "reason": "Private session handler; generation and compaction use guarded transport.", + "sha256": "9e460b4d4cd420c7692406a7ba03248b73413c830be54423fdb57fe208a6d3cb" + }, + "worker/roleplay/fallback-memory.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "af6e3d57a05edabfa704602c00b3d8e3e258a93462c19f88286127bfd86f494e" + }, + "worker/roleplay/history-reconciliation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "0012faf5281555123f48e01caa0177913e82dc030daaed1b5a9ded4b233b5694" + }, + "worker/roleplay/memory.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "9466e85cb1186b33f98fa4ae7d863071788ffc41f3e5b4bad172b4668aae8b51" + }, + "worker/roleplay/message-fragments.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "677018d928dcf2f069f483c1e09bdb4fe76179c4788262c2584b0b52477c50fc" + }, + "worker/roleplay/message-validation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "fc99aa511590f79f8b3e33a96cf679ace137a767f0c2a94dfcca2890038efdd0" + }, + "worker/roleplay/model-performance.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f13af718076e01ea366fd5c5baa9b2daf386e75ee88a8aaf76e301c30bbc8583" + }, + "worker/roleplay/model-selection.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "87e782e35c4ad4ee6e5df2c0264e9cb9b8118298bc8be0f418c270385b7ee576" + }, + "worker/roleplay/nonstream-repair.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "8d3166d81f7be6d0e6f25760a7c05866c4cd5c23eaad36300b8d7f2e3a03e196" + }, + "worker/roleplay/operator-edge.mjs": { + "kind": "internal", + "reason": "Private roleplay management/session RPC; generation enters guarded transport.", + "sha256": "24d1de1b071f8be482873ae11d3f2ce0d4eb4bfe2ff564691305e7521e718e7a" + }, + "worker/roleplay/operator-memory.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d03a800eb9fa7c74d1ed7f717d2f983a0fa70fe0521eef8998b667f68c76db20" + }, + "worker/roleplay/output-budget.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "ce1bafa71dfa0186b6685cefd0502b31fba339464d8072b1a6c36f42660c6217" + }, + "worker/roleplay/output-contract.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a1b2d7727840b0de8aa810c22584c7064954129aff17b1449b6000c124d1f17b" + }, + "worker/roleplay/profile-receipt.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f7214fe4b10d9f3d4deeee997b83f199e758e72d860586c3df21d8307db25dbd" + }, + "worker/roleplay/prompt-cache.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "aaae8c7fd84221d1cccc28896b2c25fdc3bc2393e1b10aaa836f2666357e376e" + }, + "worker/roleplay/provider-errors.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "28d78891411589354ab9f1d8a3157369dd018afaa9c7ff6875b1475afebedff1" + }, + "worker/roleplay/quality-routing.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "c3b37c7521d3813942ef7f80d94a18f8f27717bbf67a6655b23429334419db7a" + }, + "worker/roleplay/reasoning-output.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7c92345894b68d7c0fd93d7774ad4745e04b4197329fd872ab4930b6804a0591" + }, + "worker/roleplay/reasoning.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "65f296d79de04c79b0b5565a97a87b3c69111dc12c2f4b46f573c79a8c2f4dbe" + }, + "worker/roleplay/recovery.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "023fecbb3d0a4ca6310dfaeea67ccf8fa2bae5db2ac19549d7ef648535736460" + }, + "worker/roleplay/refusal-fallback.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a645bf4983ec4affddf33dff3c96b2ec14831e432b64510534dff0690cb606e0" + }, + "worker/roleplay/routing-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "648f241ab460a37b70d26e1b9a1e2349d2fc03e6e61d2a953ea9a44ac1926dbf" + }, + "worker/roleplay/session-metrics.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "39735a84caf51e261ea91a3d881281d629c9419ffb39aa94cf823f078f75444d" + }, + "worker/roleplay/session-storage.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e09f9b499a3efb77fb29390aeefeb79ad2dfb18c40b08f406c3ea91bec641ba5" + }, + "worker/roleplay/session-tier-policy.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f3c3b3b11b567ba8ab57c976bd1d3a65a7eda31a9cefc27069dd30ad07300907" + }, + "worker/roleplay/sse-collector.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "d609605183dd3f8f2b90f2f7402ba2f45afb797f88297d11460de31ed36e9997" + }, + "worker/roleplay/state-runtime.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "e59cd25c50546c28c1a7b0aaa1301d4854d6d7ad55bc9c2837ebeaae3d9f0246" + }, + "worker/roleplay/stream-validation-gate.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "457c2a917a0cc43e16567a9d28e4e43503e133f1e1be15302fa7955276d4e77b" + }, + "worker/roleplay/streaming.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "cfa1ae35acfbddf5d6f8cd362b13c24247b38df78cbca3fd0bc545924a89b372" + }, + "worker/roleplay/transport.mjs": { + "kind": "guarded", + "reason": "Every candidate, compaction and fallback uses providerFetch, including Container egress.", + "sha256": "fb1f50e58dd75196d6920a542df1c262e811a1c4e86d701d21366aa1d1540eb3" + }, + "worker/roleplay/turn-runtime.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "309d852b4164430143e4fceb4c67ce9c9fe614e2fcb35819467aad8b2cbe65a9" + }, + "worker/roleplay/turn-trace.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1cc028a2a7bef7e0e1540e1a99792e7651203b965b1562823e347c1d6474074e" + }, + "worker/roleplay/validation.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6383634dd3186799e0138897cb2023e888151062d52677d8cb89934d93583f02" + }, + "worker/route-health-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "bf916fee4d656378b95bd3f1c7239d5c7d8b2e8591ceaec64cb48a7f5b4f0772" + }, + "worker/runtime-feature-flags.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "acf379482beccbaa1faae4f1b872ee5ed613f3f3abfd600e60fb5fff1a4464fd" + }, + "worker/saml-identity.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5f1de1406099bd3c199a89913d85948f3c303f0222521fc7a6d4d527a6f296b9" + }, + "worker/scheduled-maintenance.mjs": { + "kind": "non-provider", + "reason": "Explicit maintenance webhook delivery, no provider generation.", + "sha256": "7302ed3f218a47fa3afe7a65f5e2a4353a0be24847279be128782d6a0553129e" + }, + "worker/scim-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "438e925647db4851a088c4ebe83dcb6791dc06631a384b2057a52b24284b09d6" + }, + "worker/secret-digest.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "63572c2322a6a29e602964fe2b8136cb0e563cb34aee17956ac90779f8d93396" + }, + "worker/secret-firewall.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "bd79e15c3b2d4f29916c4c2c5601284f953ff142cad80198a5155985b2560203" + }, + "worker/secret-scan.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "31a08d14c47a58ec405505b45dd254edc48f06863e1d234b10db7fb611b06a02" + }, + "worker/semantic-cache-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "1acc71757514b0e73acea66000758f4cee55e261c19b11ba5f26f608004185ff" + }, + "worker/semantic-generation-cache.mjs": { + "kind": "guarded", + "reason": "Embedding delegate is the native providerFetch wrapper, including the embedding provider authority.", + "sha256": "ba3f547d794cb9b323c6927ca65e3b45e816e73a215e97218ab023584ca68a90" + }, + "worker/shadow-eval-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "2b1a2b3b7c6551683025c80050a0d36f961c8e20106a26ba578358a97bb56e29" + }, + "worker/shadow-eval-schedule.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "b83c12aee761dfa21a0b942b8c8187e48d2af3a7dc4d015495b74d9614f03741" + }, + "worker/sse-heartbeat.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "4eae26738d0e8d7768a829396a82d83ba65792a1a9119689fd9b6031be9db670" + }, + "worker/status-page.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "af6c1272b8537020da0e435e04b1f8ac94a1bbe3d9c40eb16a24433016f5bb51" + }, + "worker/stream-cost-breaker.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5317e648a4dbb6fee35d45ae294ccee128d9296751ed1c941f578223d1be0640" + }, + "worker/tenant-governance-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "6b4f0d97520e9bfc07ee5b9879a017ccf94ca8b48a800793886f332c6933c451" + }, + "worker/tenants-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "a33aa142fea6154ce16983ad9dcdde9220ac5dbc954422abfea8d4e7a02fe50e" + }, + "worker/upstream-cancellation.mjs": { + "kind": "guarded", + "reason": "Provider adapter, protocol helper or orchestration; delegates network I/O to the guarded provider clients.", + "sha256": "44c67fd5404a2ed8acc3d74f4ccf35f1944e9bddacb44fd7177a5d829d4aef04" + }, + "worker/usage-buckets-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "7d35b0203ce30441977df2e458e825e0fb5dd0f26db81b963a0cc1c2606dc996" + }, + "worker/usage-ledger-d1.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "5430e78972b43f5257c3eddb1533586243b9e27092e0ce5517e1752232c5146e" + }, + "worker/usage-receipts.mjs": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "347edf7943143eec7bac78e79fd234a9a98940358f1839d9274abb5c7240c89b" + }, + "wsgi.py": { + "kind": "non-provider", + "reason": "Runtime module pinned independently of HTTP client spelling; network attempts require runtime admission.", + "sha256": "f07b32f2824b0b30479f475d276816223b4d4486fff067503c3ce054042e9053" + } +} diff --git a/tests/support/account_envelope_driver.py b/tests/support/account_envelope_driver.py new file mode 100644 index 00000000..21e1fa7e --- /dev/null +++ b/tests/support/account_envelope_driver.py @@ -0,0 +1,66 @@ +"""Registered Container routes, using actual HTTP to the disposable main Worker.""" +import json +import os +import sys +import tempfile +import types +from pathlib import Path + +root = str(Path(__file__).resolve().parents[2]) +sys.path.insert(0, root) +# The child receives only explicit synthetic configuration, never a host dotenv. +sys.modules["env_loader"] = types.SimpleNamespace(load_runtime_env=lambda *a, **kw: None) +os.environ.update(json.loads(sys.argv[2])) +scratch = tempfile.TemporaryDirectory(prefix="multillm-account-envelope-") +os.environ.update({ + "CONTROL_PLANE_DATABASE_URL": "", "PROVIDER_CATALOG_AUTO_REFRESH": "false", "USAGE_LEDGER_ENABLED": "false", + "AUTH_DB_PATH": scratch.name + "/auth.sqlite3", "MODEL_REGISTRY_DB_PATH": scratch.name + "/registry.sqlite3", + "RATE_LIMIT_DB_PATH": scratch.name + "/limits.sqlite3", +}) + +from services import intelligence_d1_store # noqa: E402 + +base = sys.argv[1] +for name, url in intelligence_d1_store._ENDPOINTS.items(): + intelligence_d1_store._ENDPOINTS[name] = base + url.removeprefix("http://intelligence.internal") + +import requests # noqa: E402 + +seen = [] +original = requests.sessions.Session.request + + +def transport(self, method, url, **kwargs): + if url.startswith(base + "/"): + return original(self, method, url, **kwargs) + if url.startswith("https://openrouter.ai/"): + from services.dispatch_network import network_admitted + assert network_admitted() + payload = kwargs.get("json") or json.loads(kwargs.get("data") or "{}") + if "messages" in payload: + seen.append(payload) + response = requests.Response() + response.status_code = 200 + response.headers["Content-Type"] = "application/json" + response._content = json.dumps({"data": [], "choices": [{ + "message": {"content": "synthetic"}, "finish_reason": "stop", + }]}).encode() + return response + raise AssertionError("Unexpected external request") + + +# Only the external provider is mocked. Account/control requests travel over HTTP. +requests.sessions.Session.request = transport +from app import app # noqa: E402 + +client = app.test_client() +headers = {"Authorization": "Bearer mllm_synthetic_incident_key_long_enough"} +for path, method in [("/v1/models", "get"), ("/v1/chat/completions", "post")]: + arguments = {"json": {"model": "openrouter:openai/gpt-6-luna", "messages": [ + {"role": "user", "content": "synthetic"}, + ]}} if method == "post" else {} + response = getattr(client, method)(path, headers=headers, **arguments) + print(json.dumps({"path": path, "status": response.status_code, + "error": response.json.get("error") if isinstance(response.json, dict) else None, + "dispatches": len(seen), "preferences": seen[-1].get("provider") if seen else None})) +scratch.cleanup() diff --git a/tests/support/config_revision_fixtures.mjs b/tests/support/config_revision_fixtures.mjs index 06c1af8b..d3c912a7 100644 --- a/tests/support/config_revision_fixtures.mjs +++ b/tests/support/config_revision_fixtures.mjs @@ -1,5 +1,5 @@ // Shared D1 fixtures for the config revision Worker and integration suites. -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "../helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../../worker/intelligence-outbound.mjs"; import { handleRevisionedAutoRoutes } from "../../worker/config-revision.mjs"; import { handleAutoRoutesRequest } from "../../worker/auto-routes-d1.mjs"; @@ -28,10 +28,10 @@ export const account = (changes = {}) => ({ username: "alice", api_key_hash: "sy scopes: "chat,models", is_admin: 0, created_at: "2026-10-09T00:00:00+00:00", last_login: null, last_used_at: null, last_used_ip: null, created_by: null, rotated_at: null, revoked_at: null, daily_budget_usd: null, monthly_budget_usd: null, allowed_models: null, allowed_ips: null, - expires_at: null, secret_scan_mode: null, shadow_eval_rate: null, ...changes }); + expires_at: null, secret_scan_mode: null, shadow_eval_rate: null, official_only: null, forced_provider_preferences: null, ...changes }); const usersRequest = body => new Request("http://intelligence.internal/v1/users", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }), + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }), }); export async function usersCall(env, body) { diff --git a/tests/test_account_contract_worker.mjs b/tests/test_account_contract_worker.mjs new file mode 100644 index 00000000..0756ff4c --- /dev/null +++ b/tests/test_account_contract_worker.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { accountPolicyVersion, accountReply, accountWrite } from "../worker/account-contract.mjs"; + +test("account policy negotiation is explicit and rejects unknown versions", () => { + assert.equal(accountPolicyVersion({ version: 1 }), null); + const body = { version: 1, data_policy_version: 1 }; + assert.equal(accountPolicyVersion(body), 1); + assert.deepEqual(body, { version: 1 }); + for (const value of [null, false, "1", 0, 2]) assert.throws(() => accountPolicyVersion({ data_policy_version: value })); +}); +test("legacy account projection refuses missing or malformed verification", () => { + for (const data_policy of [undefined, null, {}, { version: 1, official_only: null }]) { + assert.throws(() => accountReply({ username: "synthetic", official_only: null, + forced_provider_preferences: null, data_policy }, null)); + } + assert.throws(() => accountWrite({ username: "synthetic", official_only: 0 }, null)); + assert.deepEqual(accountWrite({ username: "synthetic" }, null), { + username: "synthetic", official_only: null, forced_provider_preferences: null, + }); +}); diff --git a/tests/test_account_envelope_integration.mjs b/tests/test_account_envelope_integration.mjs new file mode 100644 index 00000000..95ac32d4 --- /dev/null +++ b/tests/test_account_envelope_integration.mjs @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { scryptSync } from "node:crypto"; +import { build } from "esbuild"; +import { Miniflare, convertV4MiniflareOptions } from "./helpers/local_miniflare.mjs"; +import { applyMigrations } from "./d1_migrations.mjs"; +import { collectContainerEnv } from "../worker/container-env.mjs"; + +const run = promisify(execFile); +const config = JSON.parse(readFileSync(new URL("../wrangler.jsonc", import.meta.url))); +const digest = scryptSync("mllm_synthetic_incident_key_long_enough", "synthetic-salt", 64, + { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }).toString("hex"); + +test("migrated account rows cross the real private RPC and registered Container routes", async t => { + const bundle = await build({ stdin: { resolveDir: process.cwd(), contents: ` + import {handleIntelligenceOutbound} from './worker/intelligence-outbound.mjs'; + export default {fetch(request,env) { + // The private account host is owned by the main Worker, never Knowledge. + env.KNOWLEDGE_SERVICE = {fetch(){throw new Error('Old Knowledge must not receive account requests')}}; + return handleIntelligenceOutbound(new Request('http://intelligence.internal'+new URL(request.url).pathname, request),env); + }};` }, bundle: true, format: "esm", platform: "neutral", external: ["node:*", "cloudflare:*"], write: false }); + const mf = new Miniflare(convertV4MiniflareOptions({ modules: true, script: bundle.outputFiles[0].text, + compatibilityFlags: ["nodejs_compat"], d1Databases: ["INTELLIGENCE_DB"], bindings: config.vars })); + t.after(() => mf.dispose()); + const db = await mf.getD1Database("INTELLIGENCE_DB"); + await applyMigrations(db); + // Insert a pre-existing account directly, letting the actual migrations supply + // NULL defaults. No helper manufactures a policy envelope or an RPC response. + await db.prepare(`INSERT INTO control_users(username,api_key_hash,api_key_prefix,scopes,is_admin,created_at, + last_used_at,last_used_ip,created_by,secret_scan_mode,shadow_eval_rate) + VALUES('synthetic',?,?,'chat,models',0,'2026-10-01T00:00:00Z','2026-10-02T00:00:00Z', + '192.0.2.1','admin','observe',0)`) + .bind(`scrypt:32768:8:1$synthetic-salt$${digest}`, "mllm_mllm_syn").run(); + const rpc = async body => { + const response = await mf.dispatchFetch("http://local/v1/users", { method: "POST", + headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }) }); + return { status: response.status, body: await response.json() }; + }; + const containerEnv = collectContainerEnv({ ...config.vars, INTELLIGENCE_DB: db, KNOWLEDGE_SERVICE: {}, + ADMIN_API_KEY: "synthetic-bootstrap-admin-long-enough", FLASK_SECRET_KEY: "synthetic-session-secret-long-enough", + JWT_SECRET: "synthetic-jwt-secret-long-enough", OPENROUTER_API_KEY: "synthetic-operator" }); + assert.equal(containerEnv.CONFIG_REVISION_SYNC_ENABLED ?? "false", "false"); + assert.equal(containerEnv.STREAM_COST_BREAKER_ENABLED ?? "false", "false"); + const routes = async () => { + const { stdout } = await run(process.env.PYTHON || "python3", ["-I", "tests/support/account_envelope_driver.py", + (await mf.ready).href.replace(/\/$/, ""), JSON.stringify(containerEnv)], + { cwd: process.cwd(), env: { PATH: process.env.PATH }, timeout: 120000 }); + return stdout.split("\n").filter(line => line.startsWith('{"path":')).map(line => JSON.parse(line)); + }; + await t.test("unset flags and an old Knowledge binding do not break ordinary durable keys", async () => { + assert.deepEqual((await routes()).map(row => row.status), [200, 200]); + const current = await rpc({ operation: "get", username: "synthetic", data_policy_version: 1 }); + assert.deepEqual(current.body.user.data_policy, { version: 1, official_only: null, forced_provider_preferences: null }); + }); + await t.test("legacy readers receive the exact earlier row shape", async () => { + const legacy = await rpc({ operation: "get", username: "synthetic" }); + assert.equal(legacy.status, 200); + assert.equal(Object.keys(legacy.body.user).length, 19); + assert.equal(Object.hasOwn(legacy.body.user, "data_policy"), false); + assert.equal(Object.hasOwn(legacy.body.user, "official_only"), false); + const write = await rpc({ operation: "upsert", user: legacy.body.user }); + assert.equal(write.status, 200); + }); + await t.test("unsupported envelopes are refused", async () => { + assert.equal((await rpc({ operation: "get", username: "synthetic", data_policy_version: 2 })).status, 400); + }); + await db.prepare("UPDATE control_users SET official_only=1, forced_provider_preferences=? WHERE username='synthetic'") + .bind(JSON.stringify({ data_collection: "deny" })).run(); + await t.test("current controls reach the actual provider dispatch", async () => { + const rows = await routes(); + assert.deepEqual(rows.map(row => row.status), [200, 200]); + assert.equal(rows[1].preferences.data_collection, "deny"); + }); + await t.test("legacy readers and writers cannot erase or bypass controlled accounts", async () => { + assert.equal((await rpc({ operation: "get", username: "synthetic" })).status, 503); + const current = (await rpc({ operation: "get", username: "synthetic", data_policy_version: 1 })).body.user; + const legacy = Object.fromEntries(Object.entries(current).filter(([name]) => !["data_policy", "official_only", "forced_provider_preferences"].includes(name))); + assert.equal((await rpc({ operation: "upsert", user: legacy })).status, 503); + assert.equal((await rpc({ operation: "get", username: "synthetic", data_policy_version: 1 })).body.user.official_only, 1); + }); + await db.prepare("UPDATE control_users SET revoked_at='2026-10-20T00:00:00Z' WHERE username='synthetic'").run(); + await t.test("the same registered requests reject a revoked durable key", async () => { + assert.deepEqual((await routes()).map(row => row.status), [401, 401]); + }); +}); diff --git a/tests/test_ai_outbound_worker.mjs b/tests/test_ai_outbound_worker.mjs index c62d4d7a..e8177ee1 100644 --- a/tests/test_ai_outbound_worker.mjs +++ b/tests/test_ai_outbound_worker.mjs @@ -1,20 +1,22 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; import test from "node:test"; +import { providerBinding } from "./helpers/provider_binding.mjs"; import { handleAiOutbound, imageInput } from "../worker/ai-outbound.mjs"; import { collectContainerEnv } from "../worker/container-env.mjs"; function ai(result) { const calls = []; - return { calls, async run(model, input, options) { + return providerBinding({ calls, async run(model, input, options) { calls.push({ model, input, options }); if (result instanceof Error) throw result; return typeof result === "function" ? result(model, input) : result; - } }; + } }); } const post = (path, body, env) => handleAiOutbound(new Request(`http://ai.internal${path}`, { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }), env); + method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"fixture-reader"}' }, body: JSON.stringify(body) }), { INTELLIGENCE_DB: policyStore(), ...env }); test("third-party GPT Image models run through AI Gateway with OpenAI parameters", async () => { const env = { AI: ai({ result: { image: "https://images.example/out.png" } }), AI_GATEWAY_ID: "media" }; @@ -30,7 +32,7 @@ test("third-party GPT Image models run through AI Gateway with OpenAI parameters test("an image URL is downloaded when the caller asks for base64", async () => { const original = globalThis.fetch; - globalThis.fetch = async url => { assert.equal(url, "https://images.example/out.png"); return new Response(new Uint8Array([1, 2, 3])); }; + globalThis.fetch = async url => { assert.equal(url instanceof Request ? url.url : url, "https://images.example/out.png"); return new Response(new Uint8Array([1, 2, 3])); }; try { const env = { AI: ai({ image: "https://images.example/out.png" }) }; const body = await (await post("/v1/images/generations", { model: "openai/gpt-image-2", prompt: "x" }, env)).json(); diff --git a/tests/test_app_structure.py b/tests/test_app_structure.py index 3bc1aa77..713907bc 100644 --- a/tests/test_app_structure.py +++ b/tests/test_app_structure.py @@ -1,4 +1,6 @@ import json +import re +import shlex from pathlib import Path import unittest @@ -22,11 +24,28 @@ def test_ci_uses_current_worker_test_path(self): worker_test = package["scripts"]["test:worker"] self.assertIn("npm run test:worker", workflow_text) - self.assertIn("node --test tests/test_cloudflare_worker.mjs", worker_test) + self.assertIn("node --import ./tests/helpers/network_guard.mjs --test tests/test_cloudflare_worker.mjs", worker_test) self.assertIn("tests/test_roleplay_worker.mjs", worker_test) self.assertIn("tests/test_roleplay_memory_worker.mjs", worker_test) self.assertNotIn("node --test test_cloudflare_worker.mjs", workflow_text) + # Standalone Node CI steps must also run in the ordinary Worker suite. + node_job = workflow_text.split("\n node:\n", 1)[1].split("\n integration:\n", 1)[0] + worker_patterns = shlex.split(worker_test)[shlex.split(worker_test).index("--test") + 1:] + covered = {str(path.relative_to(repository)) for pattern in worker_patterns for path in repository.glob(pattern)} + for command in re.findall(r"^\s+run: (.+)$", node_job, re.MULTILINE): + if command.startswith("npm run "): + command = package["scripts"][command.removeprefix("npm run ")] + tokens = shlex.split(command) + if tokens[0] != "node" or "--test" not in tokens: + continue + for pattern in tokens[tokens.index("--test") + 1:]: + files = list(repository.glob(pattern)) + self.assertTrue(files, f"CI test pattern matches no files: {pattern}") + for path in files: + self.assertIn(str(path.relative_to(repository)), covered, + f"Node CI test is missing from test:worker: {path.name}") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_auto_routes_worker.mjs b/tests/test_auto_routes_worker.mjs index efdfaf27..bace0cf9 100644 --- a/tests/test_auto_routes_worker.mjs +++ b/tests/test_auto_routes_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; diff --git a/tests/test_cascades_worker.mjs b/tests/test_cascades_worker.mjs index 7a15b4de..20a2b928 100644 --- a/tests/test_cascades_worker.mjs +++ b/tests/test_cascades_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { validCascade } from "../worker/cascades-d1.mjs"; import { CORS_EXPOSE_HEADERS } from "../worker/cors-policy.mjs"; diff --git a/tests/test_child_safety_worker.mjs b/tests/test_child_safety_worker.mjs index fafbd730..a46669d3 100644 --- a/tests/test_child_safety_worker.mjs +++ b/tests/test_child_safety_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { handleControlUsersRequest } from "../worker/control-users-d1.mjs"; diff --git a/tests/test_client_headers_worker.mjs b/tests/test_client_headers_worker.mjs index 83d3b6a2..22fecf38 100644 --- a/tests/test_client_headers_worker.mjs +++ b/tests/test_client_headers_worker.mjs @@ -5,6 +5,7 @@ import { collectContainerEnv } from "../worker/container-env.mjs"; import { MAX_SESSION_BODY_BYTES, withOpencodeRequestSession } from "../worker/opencode-session.mjs"; import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { completionResponse, makeRoleplayEnv, roleplayRequest, withGlobalFetch } from "./helpers/roleplay_fixture.mjs"; +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; const { default: worker } = await loadWorkerModule(); @@ -80,7 +81,7 @@ test("stalled affinity discovery times out without consuming the original upload test("every direct OpenCode protocol supplies a session without harness headers", async () => { for (const path of ["/opencode/v1/chat/completions", "/opencode/v1/responses", "/opencode/v1/messages", "/opencode/v1/models"]) { - const env = { ADMIN_API_KEY: "admin-test", OPENCODE_GO_API_KEY: "opencode-test", OPENCODE_EDGE_FETCH: "true" }; + const env = policyFixtureEnv({ ADMIN_API_KEY: "admin-test", OPENCODE_GO_API_KEY: "opencode-test", OPENCODE_EDGE_FETCH: "true" }); let seen; const response = await withGlobalFetch(async (request) => { seen = request.headers; @@ -97,6 +98,20 @@ test("every direct OpenCode protocol supplies a session without harness headers" } }); +test("direct OpenCode requests refuse absent or unreadable current controls", async () => { + for (const store of [undefined, { prepare() { throw new Error("Synthetic control-store outage"); } }]) { + const env = { ADMIN_API_KEY: "admin-test", OPENCODE_GO_API_KEY: "opencode-test", OPENCODE_EDGE_FETCH: "true", + ...(store ? { INTELLIGENCE_DB: store } : {}) }; + const response = await withGlobalFetch(() => assert.fail("Unverified request reached the provider"), () => worker.fetch( + new Request("https://proxy.example/opencode/v1/chat/completions", { + method: "POST", headers: { Authorization: "Bearer admin-test", "Content-Type": "application/json" }, + body: '{"input":"Synthetic request"}', + }), env)); + assert.equal(response.status, 503); + assert.equal((await response.json()).error.code, "key_data_controls_unavailable"); + } +}); + test("Codex defaults are overridable, validated, and do not mutate caller headers", () => { const source = new Headers({ "uSeR-aGeNt": "fleet/1", Authorization: "Bearer test" }); const env = { UPSTREAM_DEFAULT_USER_AGENT: "central/2", UPSTREAM_DEFAULT_ORIGINATOR: "central" }; @@ -140,11 +155,11 @@ test("all Worker provider namespaces carry defaults and caller overrides", async requests.push(request); return new Response('{"data":[]}', { headers: { "Content-Type": "application/json" } }); }; - const env = { + const env = policyFixtureEnv({ ADMIN_API_KEY: "admin-test", OPENCODE_GO_API_KEY: "opencode-test", OPENCODE_EDGE_FETCH: "true", LINKAPI_KEY: "linkapi-test", CODEX_EASY_API_KEY: "easy-test", KIMI_CODE_API_KEY: "kimi-test", MULTILLM_PROXY_CONTAINER: { getByName: () => ({ fetch: receive }) }, - }; + }); const isGet = path.endsWith("/models"); const response = await withGlobalFetch(receive, () => worker.fetch(new Request(`https://proxy.example${path}`, { method: isGet ? "GET" : "POST", diff --git a/tests/test_cline_pass_provider.py b/tests/test_cline_pass_provider.py index 56a2e7ef..c52c0646 100644 --- a/tests/test_cline_pass_provider.py +++ b/tests/test_cline_pass_provider.py @@ -41,6 +41,12 @@ } + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class ClinePassProviderTest(unittest.TestCase): def test_chat_goes_to_the_cline_api_with_a_long_timeout(self): adapter = get_adapter("cline-pass", Config.API_BASE_URLS) diff --git a/tests/test_cloudflare_worker.mjs b/tests/test_cloudflare_worker.mjs index 233c2c60..cfc12a42 100644 --- a/tests/test_cloudflare_worker.mjs +++ b/tests/test_cloudflare_worker.mjs @@ -1,3 +1,4 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; @@ -5,7 +6,10 @@ import test from "node:test"; import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; const workerModule = await loadWorkerModule(); -const worker = workerModule.default; +const worker = { ...workerModule.default, fetch(request, env, ctx) { + return workerModule.default.fetch(request, new URL(request.url).pathname === "/ready" ? env + : { INTELLIGENCE_DB: policyStore(), ...env }, ctx); +} }; const { MultiLLMProxyContainer } = workerModule; function makeEnv(fetchImpl, envOverrides = {}) { diff --git a/tests/test_codex_easy_transport.py b/tests/test_codex_easy_transport.py index 98ae915a..1aa01e5e 100644 --- a/tests/test_codex_easy_transport.py +++ b/tests/test_codex_easy_transport.py @@ -11,6 +11,12 @@ import requests + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class CodexEasyNativeRouteTest(unittest.TestCase): def setUp(self): self.original_env = os.environ.copy() diff --git a/tests/test_config_revision_sync.py b/tests/test_config_revision_sync.py index 0971a4fd..29cce541 100644 --- a/tests/test_config_revision_sync.py +++ b/tests/test_config_revision_sync.py @@ -328,6 +328,8 @@ def account_states(changes=None): row.update(username="alice", api_key_hash=generate_password_hash("synthetic-revision-key"), api_key_prefix="mllm_syntheti", scopes="chat,models", is_admin=0, created_at="2026-10-09T00:00:00+00:00") + from services.dispatch_policy import envelope + row["data_policy"] = envelope(row) return [{"users": [row], "revisions": {"key_controls": 1, "model_grants": 1}}, {"users": [{**row, **(changes or {"revoked_at": "2026-10-09T01:00:00+00:00"})}], "revisions": {"key_controls": 2, "model_grants": 2}}] diff --git a/tests/test_config_revision_sync_worker.mjs b/tests/test_config_revision_sync_worker.mjs index 3810c679..eeb8b549 100644 --- a/tests/test_config_revision_sync_worker.mjs +++ b/tests/test_config_revision_sync_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { handleRevisionedAutoRoutes, commitRevision, revisionSyncSettings, RevisionConsumer } from "../worker/config-revision.mjs"; import { handleAutoRoutesRequest } from "../worker/auto-routes-d1.mjs"; diff --git a/tests/test_config_snapshots_worker.mjs b/tests/test_config_snapshots_worker.mjs index c15c012f..a123d3f5 100644 --- a/tests/test_config_snapshots_worker.mjs +++ b/tests/test_config_snapshots_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; diff --git a/tests/test_control_audit_worker.mjs b/tests/test_control_audit_worker.mjs index 28305100..3083029f 100644 --- a/tests/test_control_audit_worker.mjs +++ b/tests/test_control_audit_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleControlUsersRequest } from "../worker/control-users-d1.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; @@ -10,7 +10,7 @@ const user = (username, changes = {}) => ({ username, api_key_hash: hash, api_key_prefix: "mllm_abcdefgh", scopes: "chat,models", is_admin: 0, created_at: "2026-09-26T00:00:00+00:00", last_login: null, last_used_at: null, last_used_ip: null, created_by: "admin", rotated_at: null, revoked_at: null, daily_budget_usd: null, monthly_budget_usd: null, - allowed_models: null, allowed_ips: null, expires_at: null, ...changes, + allowed_models: null, allowed_ips: null, expires_at: null, official_only: null, forced_provider_preferences: null, ...changes, }); const event = (action, outcome, actor, target, detail) => ({ operation: "audit_record", action, outcome, actor, target, detail }); const list = (changes = {}) => ({ operation: "audit_list", actor: null, target: null, action: null, @@ -25,7 +25,7 @@ async function database(t) { const env = { INTELLIGENCE_DB: db, ADMIN_USERNAME: "owner" }; const call = async body => { const response = await handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }) }), env); + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }) }), env); return { status: response.status, body: await response.json() }; }; return { db, call }; @@ -132,7 +132,7 @@ test("an unavailable audit table fails closed without revealing storage details" const env = { INTELLIGENCE_DB: { prepare() { throw new Error("private database detail"); } } }; for (const body of [list(), event("sign_out", "succeeded", "owner", null, null)]) { const response = await handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }) }), env); + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }) }), env); assert.equal(response.status, 503); assert.equal((await response.text()).includes("private database detail"), false); } diff --git a/tests/test_control_state_worker.mjs b/tests/test_control_state_worker.mjs index 7a7a9324..029abdad 100644 --- a/tests/test_control_state_worker.mjs +++ b/tests/test_control_state_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; diff --git a/tests/test_control_users_worker.mjs b/tests/test_control_users_worker.mjs index 23321a44..54a9443b 100644 --- a/tests/test_control_users_worker.mjs +++ b/tests/test_control_users_worker.mjs @@ -1,6 +1,7 @@ +import { dataPolicy } from "../worker/dispatch-policy.mjs"; import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleControlUsersRequest, keyControlsPermit } from "../worker/control-users-d1.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; @@ -12,7 +13,7 @@ const user = (username, changes = {}) => ({ username, api_key_hash: hash, api_key_prefix: "mllm_abcdefgh", scopes: "knowledge:read", is_admin: 0, created_at: "2026-09-24T00:00:00+00:00", last_login: null, last_used_at: null, last_used_ip: null, created_by: "admin", rotated_at: null, revoked_at: null, daily_budget_usd: null, monthly_budget_usd: null, - allowed_models: null, allowed_ips: null, expires_at: null, secret_scan_mode: null, shadow_eval_rate: null, ...changes, + allowed_models: null, allowed_ips: null, expires_at: null, secret_scan_mode: null, shadow_eval_rate: null, official_only: null, forced_provider_preferences: null, ...changes, }); async function database(options) { @@ -21,7 +22,7 @@ async function database(options) { await applyMigrations(db, options); const call = async body => { const response = await handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }) }), { INTELLIGENCE_DB: db }); + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }) }), { INTELLIGENCE_DB: db }); return { status: response.status, body: await response.json() }; }; return { mf, db, call }; @@ -39,7 +40,7 @@ test("accounts persist, page, match active prefixes and record usage in D1", asy assert.deepEqual((await call({ operation: "list", after: "bob", limit: 2 })).body.users.map(item => item.username), ["carol"]); const active = (await call({ operation: "by_prefix", prefix: "mllm_abcdefgh" })).body.users; assert.deepEqual(active.map(item => item.username), ["alice", "carol"], "revoked accounts never authenticate"); - assert.deepEqual(Object.keys(active[0]).sort(), Object.keys(user("x")).sort()); + assert.deepEqual(Object.keys(active[0]).sort(), [...Object.keys(user("x")), "data_policy"].sort()); assert.equal((await call({ operation: "touch", username: "alice", last_used_at: "2026-09-24T02:00:00+00:00", last_used_ip: "203.0.113.9" })).body.updated, true); assert.equal((await call({ operation: "get", username: "alice" })).body.user.last_used_ip, "203.0.113.9"); assert.equal((await call({ operation: "delete", username: "carol" })).body.deleted, true); @@ -76,11 +77,11 @@ test("the users RPC is private, bounded and never reveals storage errors", async assert.equal((await handleControlUsersRequest(request("http://intelligence.internal/v1/users", "x".repeat(9000)), env)).status, 400); assert.equal((await handleControlUsersRequest(request("http://intelligence.internal/v1/users", "{}"), {})).status, 503); const failed = await handleControlUsersRequest(request("http://intelligence.internal/v1/users", - JSON.stringify({ version: 1, operation: "get", username: "alice" })), env); + JSON.stringify({ version: 1, data_policy_version: 1, operation: "get", username: "alice" })), env); assert.equal(failed.status, 503); assert.equal((await failed.text()).includes("private database detail"), false); const routed = await handleIntelligenceOutbound(request("http://intelligence.internal/v1/users", - JSON.stringify({ version: 1, operation: "get", username: "alice" })), env); + JSON.stringify({ version: 1, data_policy_version: 1, operation: "get", username: "alice" })), env); assert.equal(routed.status, 503, "the outbound router reaches the users domain"); }); @@ -98,7 +99,7 @@ test("the Worker refuses durable admin accounts it did not configure and audits const env = { INTELLIGENCE_DB: db, ADMIN_USERNAME: "owner", ADMIN_USERNAMES: "deputy, auditor" }; const send = async body => { const response = await handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, ...body }) }), env); + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }) }), env); return { status: response.status, body: await response.json() }; }; try { @@ -141,7 +142,7 @@ test("a transient D1 error on an account read is retried once", async () => { } }; const response = await handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ version: 1, operation: "get", username: "alice" }) }), { INTELLIGENCE_DB: flaky }); + body: JSON.stringify({ version: 1, data_policy_version: 1, operation: "get", username: "alice" }) }), { INTELLIGENCE_DB: flaky }); assert.equal(response.status, 200); assert.equal((await response.json()).user.username, "alice"); } finally { await mf.dispose(); } @@ -153,7 +154,7 @@ test("per-key budgets, allowlists, expiry and address ranges persist and are val const limited = user("limited", { daily_budget_usd: 5, monthly_budget_usd: 50.5, allowed_models: "auto:*,free:*,openai:gpt-4.1", allowed_ips: "203.0.113.0/24,2001:db8::/32", expires_at: "2026-12-31T00:00:00+00:00" }); assert.equal((await call({ operation: "upsert", user: limited })).status, 200); - assert.deepEqual((await call({ operation: "get", username: "limited" })).body.user, limited); + assert.deepEqual((await call({ operation: "get", username: "limited" })).body.user, { ...limited, data_policy: dataPolicy(limited) }); assert.equal((await call({ operation: "by_prefix", prefix: "mllm_abcdefgh" })).body.users[0].allowed_models, "auto:*,free:*,openai:gpt-4.1"); for (const changes of [{ daily_budget_usd: -1 }, { daily_budget_usd: "5" }, { monthly_budget_usd: 2e9 }, @@ -166,13 +167,13 @@ test("per-key budgets, allowlists, expiry and address ranges persist and are val } finally { await mf.dispose(); } }); -test("accounts keep working when code is deployed before the key-control migration", async () => { - const { mf, call } = await database({ skip: ["0007_usage_ledger.sql", "0016_usage_buckets.sql"] }); +test("management reports an unverifiable policy before the key-control migration", async () => { + const { mf, call } = await database({ skip: ["0007_usage_ledger.sql", "0016_usage_buckets.sql", "0042_usage_token_details.sql"] }); try { assert.equal((await call({ operation: "upsert", user: user("alice") })).status, 200); - assert.deepEqual((await call({ operation: "get", username: "alice" })).body.user, user("alice")); - assert.deepEqual((await call({ operation: "by_prefix", prefix: "mllm_abcdefgh" })).body.users, [user("alice")]); - assert.deepEqual((await call({ operation: "list", after: null, limit: 5 })).body.users, [user("alice")]); + assert.deepEqual((await call({ operation: "get", username: "alice" })).body.user, { ...user("alice"), data_policy: null }); + assert.deepEqual((await call({ operation: "by_prefix", prefix: "mllm_abcdefgh" })).body.users, [{ ...user("alice"), data_policy: null }]); + assert.deepEqual((await call({ operation: "list", after: null, limit: 5 })).body.users, [{ ...user("alice"), data_policy: null }]); const refused = await call({ operation: "upsert", user: user("alice", { daily_budget_usd: 1 }) }); assert.equal(refused.status, 503, "controls cannot be stored before the migration"); assert.equal((await call({ operation: "get", username: "alice" })).body.user.daily_budget_usd, null); diff --git a/tests/test_d1_integration.mjs b/tests/test_d1_integration.mjs index 54da1d5f..090c6ef2 100644 --- a/tests/test_d1_integration.mjs +++ b/tests/test_d1_integration.mjs @@ -8,7 +8,7 @@ import { existsSync } from "node:fs"; import test from "node:test"; import { promisify } from "node:util"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; @@ -72,10 +72,10 @@ test("key controls and the usage ledger persist through the Worker in D1", async test("a D1 without the account migration fails closed for dashboard keys but not for the environment admin", async t => { // 0007, 0011, 0013 and 0030 alter control_users, so they are skipped with the migration that creates the table; - // 0016 alters 0007's usage_events. + // 0016 and 0042 alter usage tables; 0041 alters control_users. const { url } = await privateStore(t, ["0003_control_users.sql", "0004_control_user_audit.sql", "0005_auto_routes.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0016_usage_buckets.sql", "0039_haiku_primary.sql", - "0030_stream_cost_caps.sql"]); + "0030_stream_cost_caps.sql", "0041_key_data_guards.sql", "0042_usage_token_details.sql"]); assert.deepEqual(await drive(url, "unmigrated"), { dashboard_key: 503, admin: "admin", route: ["gguu:gpt-image-2.5-sunburst", "gguu:gpt-image-2.5"], save_route: 503 }); }); diff --git a/tests/test_d1_schema_worker.mjs b/tests/test_d1_schema_worker.mjs index 853cb454..b0ca6a7f 100644 --- a/tests/test_d1_schema_worker.mjs +++ b/tests/test_d1_schema_worker.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { d1Readiness, REQUIRED_D1_TABLES } from "../worker/d1-schema.mjs"; import { applyMigrations, migrationNames } from "./d1_migrations.mjs"; @@ -30,7 +30,7 @@ test("/ready reports a D1 schema that was never migrated instead of forwarding", let forwarded = 0; const container = { getByName: () => ({ fetch: async () => { forwarded += 1; return Response.json({ status: "healthy" }); } }) }; // 0007 and 0030 alter control_users, so skipping 0003 skips them too; 0016 alters 0007's usage_events. - const partial = await database(t, { skip: ["0003_control_users.sql", "0005_auto_routes.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0039_haiku_primary.sql", "0016_usage_buckets.sql", "0030_stream_cost_caps.sql"] }); + const partial = await database(t, { skip: ["0003_control_users.sql", "0005_auto_routes.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0039_haiku_primary.sql", "0016_usage_buckets.sql", "0030_stream_cost_caps.sql", "0041_key_data_guards.sql", "0042_usage_token_details.sql"] }); assert.deepEqual(await d1Readiness(partial), { ready: false, checked: true, missing: ["control_users", "auto_routes", "usage_events", "usage_daily", "usage_batches", "shadow_eval_samples", "shadow_eval_results", "shadow_eval_config", "shadow_eval_policy_backups"] }); const blocked = await worker.fetch(new Request("https://gateway.example/ready"), { INTELLIGENCE_DB: partial, MULTILLM_PROXY_CONTAINER: container }); assert.equal(blocked.status, 503); diff --git a/tests/test_dashboard_scripts.mjs b/tests/test_dashboard_scripts.mjs index 48a88b95..b2579d56 100644 --- a/tests/test_dashboard_scripts.mjs +++ b/tests/test_dashboard_scripts.mjs @@ -352,7 +352,7 @@ test('usage page and key limit dialog helpers format budgets and build control p daily_budget_usd: '2.5', monthly_budget_usd: '', allowed_models: 'auto:*\nfree:*, openai:gpt-4.1', allowed_ips: ' 203.0.113.0/24 ', expires_at: '2026-12-31T23:30', }); - assert.equal(JSON.stringify(payload), JSON.stringify({ secret_scan_mode: null, shadow_eval_rate: null, daily_budget_usd: 2.5, monthly_budget_usd: null, + assert.equal(JSON.stringify(payload), JSON.stringify({ official_only: false, forced_provider_preferences: null, secret_scan_mode: null, shadow_eval_rate: null, daily_budget_usd: 2.5, monthly_budget_usd: null, allowed_models: ['auto:*', 'free:*', 'openai:gpt-4.1'], allowed_ips: ['203.0.113.0/24'], expires_at: '2026-12-31T23:30:00Z' })); for (const source of ['static/js/usage.js', 'static/js/key-controls.js']) { assert.doesNotMatch(readFileSync(source, 'utf8'), /innerHTML/, source); diff --git a/tests/test_dispatch_network.py b/tests/test_dispatch_network.py new file mode 100644 index 00000000..d6635250 --- /dev/null +++ b/tests/test_dispatch_network.py @@ -0,0 +1,62 @@ +"""Unlisted modules and alternate clients cannot escape runtime admission.""" +import socket +import _socket +from contextlib import closing + +import pytest + +from services.dispatch_network import network_admitted +from services.provider_dispatch import provider_http + + +def test_urllib3_bypass_in_an_unlisted_module_fails_before_connection(): + namespace = {} + exec(compile("import urllib3\ndef send():\n return urllib3.PoolManager(retries=False).request('GET', 'http://127.0.0.1:9')", + "unlisted_transport.py", "exec"), namespace) + with pytest.raises(AssertionError, match="outside provider chokepoint"): + namespace["send"]() + + +@pytest.mark.parametrize("call", [lambda: socket.create_connection(("127.0.0.1", 9)), + lambda: socket.socket().connect(("127.0.0.1", 9)), lambda: socket.socket().connect_ex(("127.0.0.1", 9))]) +def test_lowest_socket_layers_require_admission(call): + with pytest.raises(AssertionError, match="outside provider chokepoint"): + call() + + +@pytest.mark.parametrize("method", ["connect", "connect_ex"]) +def test_extension_socket_bypass_is_audited_before_loopback_connection(method, verified_transport_principal): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.bind(("127.0.0.1", 0)) + server.listen(1) + server.settimeout(0.02) + with closing(_socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client: + assert not network_admitted() + with pytest.raises(AssertionError, match="outside provider chokepoint"): + getattr(client, method)(server.getsockname()) + with pytest.raises(TimeoutError): + server.accept() + + def admitted(**kwargs): + with closing(_socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client: + assert network_admitted() + assert getattr(client, method)(server.getsockname()) in (None, 0) + client.sendall(b"synthetic") + connection, _ = server.accept() + with connection: + assert connection.recv(9) == b"synthetic" + provider_http("xai", "GET", "https://api.x.ai/v1/models", transport=admitted) + assert not network_admitted() + + +def test_capability_exists_only_during_admitted_transport(verified_transport_principal): + assert not network_admitted() + def send(**kwargs): + assert network_admitted() + raise RuntimeError("synthetic transport failure") + with pytest.raises(RuntimeError): + provider_http("xai", "GET", "https://api.x.ai/v1/models", transport=send) + assert not network_admitted() + + +from tests.dispatch_fixture import verified_transport_principal # noqa: E402, F401 diff --git a/tests/test_dispatch_network_worker.mjs b/tests/test_dispatch_network_worker.mjs new file mode 100644 index 00000000..ad528c90 --- /dev/null +++ b/tests/test_dispatch_network_worker.mjs @@ -0,0 +1,109 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import net from "node:net"; +import http from "node:http"; +import { readFile } from "node:fs/promises"; +import { networkAdmitted } from "../worker/dispatch-network.mjs"; +import { providerFetch, providerOperation } from "../worker/provider-dispatch.mjs"; +import { policyStore, dispatchEnv } from "./helpers/dispatch_fixture.mjs"; +import { providerBinding } from "./helpers/provider_binding.mjs"; + +const authority = { authenticated: true, keyId: "synthetic" }; + +test("SDK fixtures guard every method, nested handle and later replacement by default", async () => { + let calls = 0; + const binding = providerBinding({ run() { calls++; }, items: { get() { return { info() { calls++; } }; } } }); + assert.throws(() => binding.run(), /outside provider chokepoint/); + assert.throws(() => binding.items.get("synthetic"), /outside provider chokepoint/); + let handle; + await providerOperation("cloudflare", () => { binding.run(); handle = binding.items.get("synthetic"); handle.info(); }, + { env: dispatchEnv(), authority }); + assert.equal(calls, 2); + assert.throws(() => handle.info(), /outside provider chokepoint/); + binding.run = () => { calls++; }; + assert.throws(() => binding.run(), /outside provider chokepoint/); + assert.equal(calls, 2); +}); + +test("removing AI admission wrappers in memory fails before either SDK dispatch", async () => { + const url = new URL("../worker/ai-outbound.mjs", import.meta.url); + const source = await readFile(url, "utf8"); + let mutations = 0; + const mutated = source.replace(/providerOperation\("cloudflare", \(\) => (env\.AI\.run\([^\n]+\))\)/g, + (_, call) => { mutations++; return call; }) + .replace(/from (["'])(\.[^"']+)\1/g, (_, quote, path) => `from ${JSON.stringify(new URL(path, url).href)}`); + assert.equal(mutations, 2, "mutate both shared AI and speech dispatches"); + const { handleAiOutbound } = await import(`data:text/javascript,${encodeURIComponent(mutated)}`); + for (const [path, body] of [["embeddings", { model: "@cf/baai/bge-m3", input: "synthetic" }], + ["audio/speech", { model: "@cf/deepgram/aura-2-en", input: "synthetic" }]]) { + let calls = 0; + const env = { INTELLIGENCE_DB: policyStore(), AI: providerBinding({ run() { calls++; return {}; } }) }; + const response = await handleAiOutbound(new Request(`http://ai.internal/v1/${path}`, { + method: "POST", headers: { "content-type": "application/json", + "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"synthetic"}' }, body: JSON.stringify(body), + }), env); + assert.equal(response.status, 502, "an unadmitted SDK call cannot return success"); + assert.equal(calls, 0, "the provider double must reject before running its implementation"); + assert.equal(networkAdmitted(), false); + } +}); + +test("an unlisted axios-style adapter cannot bypass the global fetch guard", async t => { + const previous = globalThis.fetch; + t.after(() => { globalThis.fetch = previous; }); + let sends = 0; + globalThis.fetch = () => { sends++; return Response.json({}); }; + const source = "export default { get: url => fetch(url), request: config => fetch(config.url) };"; + const { default: axios } = await import(`data:text/javascript,${encodeURIComponent(source)}`); + assert.throws(() => axios.get("https://unrelated.example"), /outside provider chokepoint/); + assert.throws(() => axios.request({ url: "https://unrelated.example" }), /outside provider chokepoint/); + assert.equal(sends, 0); + await providerFetch("xai", "https://api.x.ai/v1/models", {}, { env: dispatchEnv(), authority }); + assert.equal(sends, 1); + assert.equal(networkAdmitted(), false); +}); + +test("axios-style node HTTP and direct socket adapters are guarded too", () => { + assert.throws(() => http.get("http://127.0.0.1:9"), /outside provider chokepoint/); + const socket = new net.Socket(); + try { assert.throws(() => socket.connect(9, "127.0.0.1"), /outside provider chokepoint/); } + finally { socket.destroy(); } +}); + +test("capability ends after failed HTTP and SDK transports", async () => { + const options = { env: dispatchEnv(), authority }; + await assert.rejects(providerFetch("xai", "https://api.x.ai/v1/models", {}, { ...options, send() { + assert.equal(networkAdmitted(), true); throw Error("synthetic transport failure"); + } }), /synthetic transport failure/); + assert.equal(networkAdmitted(), false); + await assert.rejects(providerOperation("cloudflare", async () => { + assert.equal(networkAdmitted(), true); throw Error("synthetic SDK failure"); + }, options), /synthetic SDK failure/); + assert.equal(networkAdmitted(), false); +}); + +test("public assets strip every credential header even with restricted controls", async () => { + const env = { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }), XAI_API_KEY: "synthetic-current" }; + await providerFetch("xai", "https://unrelated-cdn.example/asset.mp4", { headers: { + authorization: "Bearer synthetic-cached", "x-api-key": "synthetic", cookie: "synthetic", + } }, { env, authority, publicAsset: true, send(request) { + for (const name of ["authorization", "x-api-key", "cookie"]) assert.equal(request.headers.get(name), null); + return new Response("synthetic"); + } }); +}); + + +test("an API operation cannot inject operator secrets into another origin", async () => { + const env = { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }), XAI_API_KEY: "synthetic-current" }; + await assert.rejects(providerFetch("xai", "https://api.x.ai.unrelated.example/v1/videos", {}, { env, authority, + send() { assert.fail("Unapproved API destination"); } }), { code: "attested_credential_unavailable" }); +}); + +test("asynchronous work cannot retain a capability after its admitted call ends", async () => { + let release, delayed; + await providerOperation("cloudflare", async () => { + delayed = new Promise(resolve => { release = resolve; }).then(() => networkAdmitted()); + }, { env: dispatchEnv(), authority }); + release(); + assert.equal(await delayed, false); +}); diff --git a/tests/test_edge_request_metrics_worker.mjs b/tests/test_edge_request_metrics_worker.mjs index 04e713fb..9b7c37af 100644 --- a/tests/test_edge_request_metrics_worker.mjs +++ b/tests/test_edge_request_metrics_worker.mjs @@ -1,3 +1,6 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, keyId: authority?.principal?.id ?? "admin", ...authority }, ...rest); import assert from "node:assert/strict"; import test from "node:test"; import { readFile } from "node:fs/promises"; @@ -5,7 +8,7 @@ import { spawn } from "node:child_process"; import { recordNativeUsage } from "../worker/usage-ledger-d1.mjs"; import { createGatewayLifecycle } from "../worker/gateway-lifecycle.mjs"; import { createUsageObserver, observeNativeResponse, renderNativeMetrics } from "../worker/request-telemetry.mjs"; -import { nativeGenerationFetch, withForwardedCorrelation, withNativeMetrics } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch, withForwardedCorrelation, withNativeMetrics } from "../worker/gateway-extensions.mjs"; // Keep the shared loader untouched; register the owned import in this test's copy. const helperUrl = new URL("./helpers/load_cloudflare_worker.mjs", import.meta.url); @@ -13,14 +16,15 @@ const helper = (await readFile(helperUrl, "utf8")) .replaceAll("import.meta.url", JSON.stringify(helperUrl.href)) .replace("const patchedSource = source", `const patchedSource = source.replace('from "./worker/gateway-extensions.mjs";', 'from "${new URL("../worker/gateway-extensions.mjs", import.meta.url)}";')`); const registeredHelperUrl = `data:text/javascript;base64,${Buffer.from(helper).toString("base64")}`; -const worker = (await (await import(registeredHelperUrl)).loadWorkerModule()).default; +const rawWorker = (await (await import(registeredHelperUrl)).loadWorkerModule()).default; +const worker = { ...rawWorker, fetch: (request, env, ctx) => rawWorker.fetch(request, policyFixtureEnv(env), ctx) }; const encoder = new TextEncoder(); const chunks = values => new ReadableStream({ start(c) { for (const value of values) c.enqueue(encoder.encode(value)); c.close(); } }); function fixture() { const rows = [], pending = []; const env = { NATIVE_EDGE_METRICS_ENABLED: "true", ADMIN_API_KEY: "synthetic-admin", ADMIN_USERNAME: "test-owner", CODEX_EASY_API_KEY: "synthetic-upstream", INTELLIGENCE_DB: { - prepare(sql) { return { bind(...values) { return { sql, values }; } }; }, + prepare(sql) { return { bind(...values) { return { sql, values, first: async () => null }; } }; }, async batch(statements) { rows.push(...JSON.parse(statements[1].values[0])); return [{ meta: { changes: 1 } }]; }, } }; return { env, rows, ctx: { waitUntil(p) { pending.push(p); } }, async flush() { await Promise.all(pending); } }; @@ -77,7 +81,7 @@ test("parser carry stays at most 64 KiB and recovers after oversized lines", () const parser = createUsageObserver(true); parser.feed(encoder.encode('data: ' + 'x'.repeat(200000))); assert.ok(parser.carryBytes <= 65536); parser.feed(encoder.encode('\n\ndata: {"usage":{"input_tokens":0,"output_tokens":2}}\n\ndata: [DONE]\n\n')); - assert.deepEqual(parser.finish().usage, { input_tokens: 0, output_tokens: 2 }); + assert.deepEqual(parser.finish().usage, { input_tokens: 0, output_tokens: 2, cached_input_tokens: null, reasoning_tokens: null }); }); test("partial usage, missing usage, stream error and cancellation remain unknown", async () => { @@ -207,7 +211,7 @@ test("explicit estimate collaborator retains measured zero and marks estimated c test("multiline UTF-8 SSE and large JSON keep bounds and original bytes", async () => { const parser = createUsageObserver(true), raw = 'data: {"usage":\r\ndata: {"input_tokens":0,"output_tokens":0}}\r\n\r\ndata: [DONE]\r\n\r\n'; const bytes = encoder.encode(raw); for (const byte of bytes) { parser.feed(new Uint8Array([byte])); assert.ok(parser.carryBytes <= 65536); } - assert.deepEqual(parser.finish().usage, { input_tokens: 0, output_tokens: 0 }); + assert.deepEqual(parser.finish().usage, { input_tokens: 0, output_tokens: 0, cached_input_tokens: null, reasoning_tokens: null }); const events = [], big = JSON.stringify({ output: "漢".repeat(40000), usage: { input_tokens: 2, output_tokens: 3 } }); const response = await observeNativeResponse(new Response(big), { provider: "linkapi" }, { finalize: event => events.push(event) }); assert.equal(await response.text(), big); assert.equal(events[0].usage_basis, "unknown"); assert.equal(events[0].cost_usd, null); @@ -223,13 +227,13 @@ test("all finalizers run once even if a collaborator fails", async () => { test("native ledger statements preserve nullable usage and batch idempotency in SQLite", async () => { const statements = []; const env = { INTELLIGENCE_DB: { - prepare(sql) { return { bind(...values) { return { sql, values }; } }; }, + prepare(sql) { return { bind(...values) { return { sql, values, first: async () => null }; } }; }, async batch(batch) { statements.push(...batch); return [{ meta: { changes: 1 } }]; }, } }; const event = { provider: "codex-easy", model: "test-model", principal: "edge:trusted", endpoint: "/codex-easy/v1/chat/completions", status: 520, duration_ms: 9, input_tokens: 0, output_tokens: null, cost_usd: null, cost_basis: null, requestId: crypto.randomUUID() }; await recordNativeUsage(env, event); await recordNativeUsage(env, event); - const schema = (await Promise.all(["0003_control_users.sql", "0007_usage_ledger.sql"].map(name => + const schema = (await Promise.all(["0003_control_users.sql", "0007_usage_ledger.sql", "0042_usage_token_details.sql"].map(name => readFile(new URL(`../intelligence-migrations/${name}`, import.meta.url), "utf8")))).join("\n"); const code = `import json,sqlite3,sys payload=json.loads(sys.argv[1]) diff --git a/tests/test_evaluation_noise_floor_worker.mjs b/tests/test_evaluation_noise_floor_worker.mjs index 33604fb2..17ff1e0e 100644 --- a/tests/test_evaluation_noise_floor_worker.mjs +++ b/tests/test_evaluation_noise_floor_worker.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; -import { convertV4MiniflareOptions, Miniflare } from 'miniflare'; +import { convertV4MiniflareOptions, Miniflare } from './helpers/local_miniflare.mjs'; import { applyMigrations } from './d1_migrations.mjs'; import { handleShadowEvalRequest, validResult } from '../worker/shadow-eval-d1.mjs'; diff --git a/tests/test_free_routes_worker.mjs b/tests/test_free_routes_worker.mjs index c97622d9..b709747a 100644 --- a/tests/test_free_routes_worker.mjs +++ b/tests/test_free_routes_worker.mjs @@ -4,6 +4,7 @@ import test from "node:test"; import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { collectContainerEnv } from "../worker/container-env.mjs"; +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; const worker = (await loadWorkerModule()).default; const origin = "https://example.test"; @@ -15,7 +16,7 @@ test("deployments preserve operator-owned free-tier settings", async () => { }); function environment(fetch) { - return { MULTILLM_PROXY_CONTAINER: { getByName: () => ({ fetch }) } }; + return policyFixtureEnv({ ADMIN_API_KEY: "test-proxy-key", MULTILLM_PROXY_CONTAINER: { getByName: () => ({ fetch }) } }); } test("free pool preflight is answered at the edge without starting a container", async () => { diff --git a/tests/test_gemini_data_guards.py b/tests/test_gemini_data_guards.py new file mode 100644 index 00000000..a74c07f8 --- /dev/null +++ b/tests/test_gemini_data_guards.py @@ -0,0 +1,122 @@ +"""Paid-tier admission stays bound to the operator credential on every path.""" + +import json +import os +from unittest.mock import patch + +import requests + +from services.auto_route_service import AutoRoute +from tests.unified_api_test_case import UnifiedApiTestCase + + +class GeminiDataRouteTest(UnifiedApiTestCase): + def setUp(self): + super().setUp() + self.app.config["GEMINI_DATA_TIER"] = "paid" + os.environ["GEMINI_API_KEY"] = "AIza-attested-synthetic" + os.environ["OPENAI_API_KEY"] = "synthetic-operator" + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("guarded", is_admin=True) + auth.set_key_controls("guarded", {"official_only": True}) + self.key = created["api_key"] + + def endpoints(self): + return [("/gemini/v1beta/models/gemini-test-model:generateContent", {"contents": [{"parts": [{"text": "Synthetic"}]}]}), + ("/gemini/chat/completions", {"model": "gemini-test-model", "messages": [{"role": "user", "content": "Synthetic"}]}), + ("/v1/chat/completions", {"model": "gemini:gemini-test-model", "messages": [{"role": "user", "content": "Synthetic"}]})] + + @staticmethod + def native_response(): + response = requests.Response() + response.status_code = 200 + response._content = json.dumps({"candidates": [{"content": {"parts": [{"text": "Synthetic"}]}, "finishReason": "STOP"}]}).encode() + response.headers["Content-Type"] = "application/json" + return response + + def test_raw_native_and_unified_refuse_caller_credentials_before_upstream(self): + overrides = [("?key=AIza-caller-synthetic", {}), ("?%6bey=", {}), + ("", {"x-goog-api-key": "AIza-caller-synthetic"}), + ("", {"X-MultiLLM-Api-Key": self.key, "Authorization": "Bearer AIza-caller-synthetic"})] + with patch.object(self.app_module.ProxyService, "_make_base_request") as send: + for endpoint, payload in self.endpoints(): + for query, extra in overrides: + with self.subTest(endpoint=endpoint, override=query or next(iter(extra))): + response = self.client.post(endpoint + query, json=payload, + headers={"Authorization": f"Bearer {self.key}", **extra}) + assert response.status_code == 403, response.json + assert response.json["error"] == "provider_credential_not_allowed" + send.assert_not_called() + + def test_every_route_uses_only_the_attested_operator_key(self): + with patch.object(self.app_module.ProxyService, "_make_base_request", side_effect=lambda **_: self.native_response()) as send: + for endpoint, payload in self.endpoints(): + with self.subTest(endpoint=endpoint): + response = self.client.post(endpoint, json=payload, headers={"Authorization": f"Bearer {self.key}"}) + assert response.status_code == 200, response.json + headers = send.call_args.kwargs["headers"] + assert headers["x-goog-api-key"] == "AIza-attested-synthetic" + assert not any(name.lower() == "authorization" for name in headers) + + def test_missing_attested_binding_never_uses_cached_or_alias_credentials(self): + os.environ.pop("GEMINI_API_KEY") + with patch.object(self.app_module.AuthService, "get_api_key", return_value="AIza-unattested-synthetic"), \ + patch.object(self.app_module.ProxyService, "_make_base_request") as send: + for endpoint, payload in self.endpoints(): + with self.subTest(endpoint=endpoint): + response = self.client.post(endpoint, json=payload, headers={"Authorization": f"Bearer {self.key}"}) + assert response.status_code == 503, response.json + assert response.json["error"] == "attested_credential_unavailable" + send.assert_not_called() + + def test_automatic_fallback_refuses_overrides_and_missing_attestation(self): + from services.auto_route_service import AutoRouteService + route = AutoRoute("auto:attested", ("openai:first", "gemini:gemini-test-model"), "synthetic") + calls = [] + def send(**kwargs): + calls.append(kwargs) + assert kwargs["api_provider"] == "openai" + response = requests.Response() + response.status_code = 429 + response._content = b'{"error":{"message":"Synthetic limit"}}' + response.headers["Content-Type"] = "application/json" + return response + with patch.object(AutoRouteService, "get_route", side_effect=lambda name: route if name == route.id else None), \ + patch.object(self.app_module.ProxyService, "_make_base_request", side_effect=send): + for query, missing in [("?key=AIza-caller-synthetic", False), ("", True)]: + with self.subTest(missing=missing): + if missing: + os.environ.pop("GEMINI_API_KEY") + calls.clear() + response = self.client.post("/v1/chat/completions" + query, + json={"model": route.id, "messages": [{"role": "user", "content": "Synthetic"}]}, + headers={"Authorization": f"Bearer {self.key}"}) + assert response.status_code >= 400, response.json + assert len(calls) == 1, (response.status_code, response.json) + + +def test_transport_boundary_blocks_overrides_even_on_forced_raw_dispatch(monkeypatch): + import pytest + from flask import Flask, g + from error_handlers import APIError + from services.proxy_service import ProxyService + + monkeypatch.setenv("GEMINI_API_KEY", "AIza-attested-synthetic") + app = Flask(__name__) + app.config["GEMINI_DATA_TIER"] = "paid" + url = "https://provider.example/v1beta/models/example:generateContent" + overrides = [(url, {"key": "AIza-caller-synthetic"}, {}), + (url + "?key=AIza-caller-synthetic", {}, {}), + (url, {}, {"x-goog-api-key": "AIza-caller-synthetic"}), + (url, {}, {"authorization": "Bearer AIza-caller-synthetic"})] + with app.test_request_context(headers={"Authorization": "Bearer synthetic-gateway"}): + g.authenticated_user = {"official_only": True} + with patch.object(ProxyService, "_make_base_request") as send: + for force_raw in (False, True): + for target, params, headers in overrides: + with pytest.raises(APIError) as error: + ProxyService.make_request("POST", target, headers, params, b"{}", "gemini", + use_cache=False, force_raw_passthrough=force_raw) + assert error.value.status_code == 403 + send.assert_not_called() diff --git a/tests/test_generation_deadline.py b/tests/test_generation_deadline.py index ba2fa9c0..45a21642 100644 --- a/tests/test_generation_deadline.py +++ b/tests/test_generation_deadline.py @@ -11,6 +11,11 @@ from services.resilience_service import ResilienceService + +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class Clock: def __init__(self): self.now = 100.0 diff --git a/tests/test_gpt_image_moderation_worker.mjs b/tests/test_gpt_image_moderation_worker.mjs index f8465bbc..afa2c725 100644 --- a/tests/test_gpt_image_moderation_worker.mjs +++ b/tests/test_gpt_image_moderation_worker.mjs @@ -1,7 +1,14 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; const worker = (await loadWorkerModule()).default; diff --git a/tests/test_haiku_intelligence_worker.mjs b/tests/test_haiku_intelligence_worker.mjs index 9f1f3a6e..55124139 100644 --- a/tests/test_haiku_intelligence_worker.mjs +++ b/tests/test_haiku_intelligence_worker.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { applyMigrations } from "./d1_migrations.mjs"; diff --git a/tests/test_handoff_script.py b/tests/test_handoff_script.py index 1e88832a..5a3295b5 100644 --- a/tests/test_handoff_script.py +++ b/tests/test_handoff_script.py @@ -14,6 +14,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from unittest.mock import Mock, patch +from services.dispatch_network import control_network + import pytest from scripts import handoff import handoff_contracts as contracts @@ -83,7 +85,8 @@ def do_POST(self): thread = threading.Thread(target=http.serve_forever, daemon=True) thread.start() state["base"] = f"http://127.0.0.1:{http.server_port}" - yield state + with control_network(state["base"]): + yield state http.shutdown() http.server_close() thread.join(2) diff --git a/tests/test_intelligence_auth_worker.mjs b/tests/test_intelligence_auth_worker.mjs index 4cd639dd..91fb626b 100644 --- a/tests/test_intelligence_auth_worker.mjs +++ b/tests/test_intelligence_auth_worker.mjs @@ -1,7 +1,7 @@ import {test} from "node:test"; import assert from "node:assert/strict"; import {readFile} from "node:fs/promises"; -import {convertV4MiniflareOptions,Miniflare} from "miniflare"; +import {convertV4MiniflareOptions,Miniflare} from "./helpers/local_miniflare.mjs"; import {handleIntelligenceAuthRequest} from "../worker/intelligence-auth-d1.mjs"; const hash = "scrypt:32768:8:1$salt123456789012$" + "a".repeat(128); @@ -23,6 +23,16 @@ test("D1 credentials survive new callers, rotate atomically and retain revocatio assert.equal((await call(provision)).status,409); const lookup = async keyPrefix => (await (await call({operation:"lookup",keyPrefix})).json()).principal; assert.equal((await lookup(prefix)).id,"integration:omni"); + const dispatchPolicy = () => call({operation:"dispatch_policy",principalId:"integration:omni"}); + assert.equal((await dispatchPolicy()).status,503); + const policyMigration = await readFile(new URL("../intelligence-migrations/0043_integration_data_policy.sql", import.meta.url),"utf8"); + for (const statement of policyMigration.split(";").filter(x=>x.trim())) await db.prepare(statement).run(); + assert.deepEqual((await (await dispatchPolicy()).json()).principal.data_policy, + {version:1,official_only:null,forced_provider_preferences:null}); + await db.prepare("UPDATE intelligence_principals SET official_only=1, forced_provider_preferences=? WHERE id=?") + .bind('{"data_collection":"deny"}',"integration:omni").run(); + assert.deepEqual((await (await dispatchPolicy()).json()).principal.data_policy, + {version:1,official_only:1,forced_provider_preferences:{data_collection:"deny"}}); const next = "mllm_intelligence_" + "b".repeat(16); const rotate = {operation:"rotate",principalId:"integration:omni",expectedVersion:1,keyPrefix:next,keyHash:hash}; const races = await Promise.all([call(rotate),call(rotate)]); @@ -32,6 +42,7 @@ test("D1 credentials survive new callers, rotate atomically and retain revocatio assert.equal((await call({...rotate,expectedVersion:2,keyPrefix:prefix})).status,409); assert.equal((await call({operation:"revoke",principalId:"integration:omni",expectedVersion:2})).status,200); assert.ok((await lookup(next)).revokedAt); + assert.equal((await (await dispatchPolicy()).json()).principal,null); assert.equal((await call({...rotate,expectedVersion:2,keyPrefix:"mllm_intelligence_"+"c".repeat(16)})).status,409); assert.equal((await call(provision)).status,409); } finally { await mf.dispose(); } diff --git a/tests/test_intelligence_cancellation.py b/tests/test_intelligence_cancellation.py index 65242cec..4b04179c 100644 --- a/tests/test_intelligence_cancellation.py +++ b/tests/test_intelligence_cancellation.py @@ -9,6 +9,7 @@ from werkzeug.serving import make_server from services.control_plane_backup import capture +from services.dispatch_network import control_network from services.intelligence_cancellation import CallerCancellation from tests.intelligence_fixtures import IntelligenceApiTestCase, upstream @@ -65,7 +66,7 @@ def blocked_provider(**kwargs): rows = [] try: with self.requests(side_effect=blocked_provider) as send: - with socket.create_connection( + with control_network(f"http://127.0.0.1:{server.server_port}"), socket.create_connection( ("127.0.0.1", server.server_port) ) as client: client.sendall(wire) diff --git a/tests/test_intelligence_d1_worker.mjs b/tests/test_intelligence_d1_worker.mjs index 55f71ee0..eebade48 100644 --- a/tests/test_intelligence_d1_worker.mjs +++ b/tests/test_intelligence_d1_worker.mjs @@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceStoreRequest } from "../worker/intelligence-d1.mjs"; const source = await readFile(new URL("../worker/intelligence-d1.mjs", import.meta.url), "utf8"); diff --git a/tests/test_intelligence_operator.mjs b/tests/test_intelligence_operator.mjs index bedeab92..05b8f2b6 100644 --- a/tests/test_intelligence_operator.mjs +++ b/tests/test_intelligence_operator.mjs @@ -3,7 +3,7 @@ import { lstat, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceAuthRequest } from "../worker/intelligence-auth-d1.mjs"; import { generateIntegrationCredential, hashIntegrationKey, openRemoteDatabase, runOperator, diff --git a/tests/test_key_controls.py b/tests/test_key_controls.py index 97fc39d5..729ab7b6 100644 --- a/tests/test_key_controls.py +++ b/tests/test_key_controls.py @@ -20,7 +20,7 @@ def test_validation_normalizes_every_control(): "daily_budget_usd": 5.0, "monthly_budget_usd": 50.123457, "allowed_models": "auto:*,free:*,openai:gpt-4.1", "allowed_ips": "203.0.113.7/32,198.51.100.0/24,2001:db8::/32,192.0.2.1/32", - "expires_at": "2026-10-01T12:00:00+00:00", "secret_scan_mode": None, "shadow_eval_rate": None, + "expires_at": "2026-10-01T12:00:00+00:00", "secret_scan_mode": None, "shadow_eval_rate": None, "official_only": None, "forced_provider_preferences": None, } assert key_controls.validate({}) == key_controls.empty() assert key_controls.validate({"allowed_models": [], "allowed_ips": "", "daily_budget_usd": None}) == key_controls.empty() @@ -132,7 +132,7 @@ def test_sqlite_accounts_keep_controls_through_rotation_and_backups(tmp_path, mo def test_stored_values_are_read_defensively(): row = {"daily_budget_usd": 2, "monthly_budget_usd": "9", "allowed_models": "", "allowed_ips": "10.0.0.0/8", - "expires_at": None} + "expires_at": None, "official_only": None, "forced_provider_preferences": None} assert key_controls.from_storage(row) == {"daily_budget_usd": 2.0, "monthly_budget_usd": None, - "allowed_models": None, "allowed_ips": "10.0.0.0/8", "expires_at": None, "secret_scan_mode": None, "shadow_eval_rate": None} + "allowed_models": None, "allowed_ips": "10.0.0.0/8", "expires_at": None, "secret_scan_mode": None, "shadow_eval_rate": None, "official_only": None, "forced_provider_preferences": None} assert key_controls.public(row)["allowed_ips"] == ["10.0.0.0/8"] diff --git a/tests/test_key_data_guards.py b/tests/test_key_data_guards.py new file mode 100644 index 00000000..5bfddf71 --- /dev/null +++ b/tests/test_key_data_guards.py @@ -0,0 +1,152 @@ +"""Data controls protect an app whose requests carry personal data.""" + +import json +from types import SimpleNamespace +from unittest.mock import patch + +import pytest +from flask import Flask, Response, g + +from error_handlers import APIError +from services import key_controls, key_data_guards +from tests.unified_api_test_case import UnifiedApiTestCase + + +def test_controls_validate_storage_and_public_values(): + controls = key_controls.validate({"official_only": True, + "forced_provider_preferences": {"data_collection": "deny", "zdr": True}}) + assert controls["official_only"] == 1 + restored = key_controls.from_storage(controls) + assert key_controls.public(restored)["official_only"] is True + assert key_controls.public(restored)["forced_provider_preferences"] == {"data_collection": "deny", "zdr": True} + for payload in ({"official_only": "true"}, {"official_only": 1}, + {"forced_provider_preferences": []}, {"forced_provider_preferences": "oops"}, + {"forced_provider_preferences": {"data_collection": "invalid"}}, + {"forced_provider_preferences": {"data_collection": []}}, + {"forced_provider_preferences": {"only": ["x" * 5000]}}): + with pytest.raises(APIError) as error: + key_controls.validate(payload) + assert error.value.status_code == 400 + with pytest.raises(APIError) as error: + key_controls.from_storage({"official_only": 2}) + assert error.value.status_code == 503 + + +def test_registry_classes_and_tiers_fail_closed(): + from config import Config + from providers.registry import get_registry + registry = get_registry(Config.API_BASE_URLS) + for name, adapter in registry.items(): + assert adapter.data_class in {"official", "subscription", "reseller", "free"}, name + assert key_data_guards.provider_data_class("openai") == "official" + assert key_data_guards.provider_data_class("ce-unlisted") == "subscription" + assert key_data_guards.provider_data_class("custom-relay") == "reseller" + assert key_data_guards.provider_data_class("gemini", {}) == "free" + assert key_data_guards.provider_data_class("gemini", {"GEMINI_DATA_TIER": "paid"}) == "official" + assert key_data_guards.provider_data_class("gemini", {"GEMINI_DATA_TIER": "paid", + "FREE_ROUTE_FREE_TIER_PROVIDERS": "gemini"}) == "free" + assert not key_controls.model_allowed({"official_only": True}, "free:text") + assert key_controls.model_allowed({"official_only": True}, "auto:chat") + + +@pytest.mark.parametrize("caller,forced,expected", [ + ({"data_collection": "allow", "sort": "price"}, {"data_collection": "deny"}, + {"data_collection": "deny", "sort": "price"}), + ({"data_collection": "deny"}, {"data_collection": "allow"}, {"data_collection": "deny"}), + ({"max_price": {"prompt": 4, "completion": 2}}, {"max_price": {"prompt": 1}}, + {"max_price": {"prompt": 1, "completion": 2}}), +]) +def test_preferences_cannot_be_weakened_at_transport(caller, forced, expected): + app = Flask(__name__) + with app.test_request_context(): + g.authenticated_user = {"forced_provider_preferences": forced} + original = {"model": "example/model", "provider": caller} + data = json.dumps(original).encode() + guarded = key_data_guards.guard_request("openrouter", data) + assert json.loads(guarded)["provider"] == expected + assert json.loads(data) == original + assert key_data_guards.guard_request("openai", data) == data + + +def test_auto_filters_candidates_before_dispatch_and_refuses_empty(): + from routes.auto_routes import dispatch_auto_route + app = Flask(__name__) + route = SimpleNamespace(id="auto:guarded", candidates=("opencode:example", "openai:example"), canary=None) + calls = [] + with app.test_request_context(), patch("routes.auto_routes.AutoRouteService.get_route", return_value=route), \ + patch("routes.auto_routes.canary_traffic.prepare_request", return_value=None): + g.authenticated_user = {"official_only": True} + response = dispatch_auto_route({"model": route.id}, validate_candidate=lambda _: None, + dispatch_candidate=lambda payload, model, decision: (calls.append(model) or Response("{}", mimetype="application/json"))) + assert response.status_code == 200 + assert calls == ["openai:example"] + route.candidates = ("opencode:example",) + with pytest.raises(APIError) as error: + dispatch_auto_route({"model": route.id}, validate_candidate=lambda _: None, + dispatch_candidate=lambda *_: pytest.fail("No eligible provider")) + assert error.value.status_code == 403 + assert error.value.payload["error"] == "provider_class_not_allowed" + + +def test_cascade_filters_tiers_and_nested_auto_without_dispatch(): + from routes.cascades import dispatch_cascade, validate_cascade_target + app = Flask(__name__) + config = {"name": "cascade:guarded", "tiers": [{"model": "opencode:example"}], "checks": ["complete"]} + with app.test_request_context(), patch("routes.cascades.CascadeService.get_route", return_value=config): + g.authenticated_user = {"official_only": True} + with pytest.raises(APIError) as error: + dispatch_cascade({"model": config["name"]}, lambda *_: pytest.fail("No eligible tier")) + assert error.value.payload["error"] == "provider_class_not_allowed" + config["tiers"] = [{"model": "auto:guarded"}] + route = SimpleNamespace(id="auto:guarded", candidates=("opencode:example",)) + with patch("routes.unified.AutoRouteService.get_route", return_value=route): + with pytest.raises(APIError) as error: + validate_cascade_target(app, None, config["name"], None) + assert error.value.status_code == 403 + + +def test_cascade_uses_remaining_official_tier(monkeypatch): + from tests.test_cascades import CONFIG, answer, run + from services import request_accounting, usage_ledger + app = Flask(__name__) + monkeypatch.setattr(request_accounting, "begin", lambda **_: None) + monkeypatch.setattr(usage_ledger.LEDGER, "record", lambda _: None) + with app.test_request_context(): + g.authenticated_user = {"official_only": True} + config = {**CONFIG, "tiers": [{"model": "opencode:example"}, {"model": "openai:example"}]} + response, calls = run(config, [answer()]) + assert response.status_code == 200 + assert [body["model"] for body in calls] == ["openai:example"] + + +class KeyDataRouteTest(UnifiedApiTestCase): + def key(self, **controls): + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("guarded", is_admin=True) + auth.set_key_controls("guarded", controls) + return created["api_key"] + + def test_direct_classes_and_free_routes_refuse_before_transport(self): + key = self.key(official_only=True) + with patch("app.ProxyService.make_request") as transport: + for endpoint, model in (("/v1/chat/completions", "opencode:glm-5.2"), + ("/v1/chat/completions", "free:text"), + ("/linkapi/v1/chat/completions", "example/model")): + response = self.client.post(endpoint, json={"model": model, "messages": []}, + headers={"Authorization": f"Bearer {key}"}) + assert response.status_code == 403, response.get_json() + assert response.get_json()["error"] == "provider_class_not_allowed" + transport.assert_not_called() + + def test_transport_merges_forced_preferences_on_unified_and_raw_calls(self): + key = self.key(forced_provider_preferences={"data_collection": "deny"}) + with patch.object(self.app_module.ProxyService, "_make_base_request", return_value=self._chat_response()) as upstream, \ + patch.object(self.app_module.AuthService, "get_api_key", return_value="synthetic-provider-key"): + for endpoint, model in (("/v1/chat/completions", "openrouter:example/model"), + ("/openrouter/chat/completions", "example/model")): + response = self.client.post(endpoint, headers={"Authorization": f"Bearer {key}"}, + json={"model": model, "messages": [{"role": "user", "content": "example"}], + "provider": {"data_collection": "allow", "sort": "price"}}) + assert response.status_code == 200, response.get_json() + assert json.loads(upstream.call_args.kwargs["data"])["provider"] == {"data_collection": "deny", "sort": "price"} diff --git a/tests/test_key_data_guards_worker.mjs b/tests/test_key_data_guards_worker.mjs new file mode 100644 index 00000000..e8dcdc12 --- /dev/null +++ b/tests/test_key_data_guards_worker.mjs @@ -0,0 +1,85 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; +import { dataPolicy } from "../worker/dispatch-policy.mjs"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; +import { guardNativeRequest, mergePreferences, providerDataClass, validDataControls } from "../worker/key-data-guards.mjs"; +import { nativeGenerationFetch } from "../worker/gateway-extensions.mjs"; +import { handleControlUsersRequest } from "../worker/control-users-d1.mjs"; +import { applyMigrations } from "./d1_migrations.mjs"; + +const request = (provider = "openrouter", preferences = { data_collection: "allow" }) => new Request(`https://example.test/${provider}/v1/chat/completions`, { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "example/model", provider: preferences, messages: [{ role: "user", content: "example" }] }), +}); +const authority = (provider, principal) => ({ authenticated: true, provider, principal, keyId: principal.id ?? "example", + route: `/${provider}/v1/chat/completions` }); + +async function database(t) { + const mf = new Miniflare(convertV4MiniflareOptions({ modules: true, + script: "export default {fetch(){return new Response('ok')}}", d1Databases: ["INTELLIGENCE_DB"] })); + t.after(() => mf.dispose()); + const db = await mf.getD1Database("INTELLIGENCE_DB"); + await applyMigrations(db); + return { INTELLIGENCE_DB: db }; +} + +test("provider classes and explicit tier choices exclude unverified endpoints", () => { + assert.equal(providerDataClass("openai"), "official"); + assert.equal(providerDataClass("opencode"), "subscription"); + assert.equal(providerDataClass("nanogpt"), "reseller"); + assert.equal(providerDataClass("free"), "free"); + assert.equal(providerDataClass("custom"), "reseller"); + assert.equal(providerDataClass("ce-custom"), "subscription"); + assert.equal(providerDataClass("gemini"), "free"); + assert.equal(providerDataClass("gemini", { GEMINI_DATA_TIER: "paid" }), "official"); + assert.equal(providerDataClass("gemini", { GEMINI_DATA_TIER: "paid", FREE_ROUTE_FREE_TIER_PROVIDERS: "gemini" }), "free"); + for (const value of [2, true, "true"]) assert.equal(validDataControls({ official_only: value }), false); + assert.equal(validDataControls({ forced_provider_preferences: "[]" }), false); + assert.equal(validDataControls({ forced_provider_preferences: '{"data_collection":"invalid"}' }), false); +}); + +test("native direct requests refuse before calling any upstream", async () => { + for (const provider of ["opencode", "nanogpt", "gemini", "ce-gpt-pro", "free"]) { + const response = await nativeGenerationFetch(request(provider), { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }) }, {}, + authority(provider, { official_only: 1 }), () => assert.fail("Must not dispatch")); + assert.equal(response.status, 403); + assert.equal((await response.json()).error.code, "provider_class_not_allowed"); + } + let calls = 0; + const response = await nativeGenerationFetch(new Request("https://api.openai.com/v1/chat/completions", request("openai")), { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }), OPENAI_API_KEY: "synthetic-operator" }, {}, + authority("openai", { official_only: 1 }), async () => { calls++; return new Response("{}"); }); + assert.equal(response.status, 200); + assert.equal(calls, 1); +}); + +test("forced preferences override caller allow and preserve caller deny", async () => { + const principal = { forced_provider_preferences: '{"data_collection":"deny","max_price":{"prompt":1}}' }; + const guarded = await guardNativeRequest(request("openrouter", { data_collection: "allow", sort: "price", max_price: { completion: 2 } }), { INTELLIGENCE_DB: policyStore({ official_only: null, forced_provider_preferences: principal.forced_provider_preferences }) }, authority("openrouter", principal)); + assert.deepEqual((await guarded.request.json()).provider, + { data_collection: "deny", sort: "price", max_price: { completion: 2, prompt: 1 } }); + assert.deepEqual(mergePreferences({ data_collection: "deny" }, { data_collection: "allow" }), { data_collection: "deny" }); + const pollution = mergePreferences({}, JSON.parse('{"__proto__":{"polluted":true}}')); + assert.equal({}.polluted, undefined); + assert.equal(Object.hasOwn(pollution, "__proto__"), true); +}); + +test("durable key controls round-trip and native dispatch reads them", async t => { + const env = await database(t); + const user = { username: "example", api_key_hash: "synthetic-hash", api_key_prefix: "mllm_example", scopes: "chat,models", + is_admin: 0, created_at: "2026-10-20T00:00:00Z", last_login: null, last_used_at: null, last_used_ip: null, + created_by: null, rotated_at: null, revoked_at: null, daily_budget_usd: null, monthly_budget_usd: null, + allowed_models: null, allowed_ips: null, expires_at: null, secret_scan_mode: null, shadow_eval_rate: null, + official_only: 1, forced_provider_preferences: '{"data_collection":"deny"}' }; + const call = body => handleControlUsersRequest(new Request("http://intelligence.internal/v1/users", { + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, data_policy_version: 1, ...body }), + }), env); + assert.equal((await call({ operation: "upsert", user })).status, 200); + assert.deepEqual((await (await call({ operation: "get", username: user.username })).json()).user, { ...user, data_policy: dataPolicy(user) }); + const denied = await guardNativeRequest(request("opencode"), env, authority("opencode", { id: user.username })); + assert.equal(denied.response.status, 403); + const guarded = await guardNativeRequest(request(), env, authority("openrouter", { id: user.username })); + assert.equal((await guarded.request.json()).provider.data_collection, "deny"); + const unavailable = await guardNativeRequest(request(), { INTELLIGENCE_DB: { prepare() { throw new Error(); } } }, authority("openrouter", { id: user.username })); + assert.equal(unavailable.response.status, 503); +}); diff --git a/tests/test_key_data_intelligence.py b/tests/test_key_data_intelligence.py new file mode 100644 index 00000000..b1031f71 --- /dev/null +++ b/tests/test_key_data_intelligence.py @@ -0,0 +1,66 @@ +"""Key controls survive managed routing and its background transport handoff.""" + +import json +from unittest.mock import patch + +from services.intelligence_store import IntelligenceStore +from tests.intelligence_fixtures import IntelligenceApiTestCase, completion, upstream +from tests.test_intelligence_policy import candidate, policy + + +class KeyDataIntelligenceTest(IntelligenceApiTestCase): + def controlled_key(self, **controls): + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("guarded", scopes=["chat", "models"]) + auth.set_key_controls("guarded", controls) + self.headers["Authorization"] = f"Bearer {created['api_key']}" + + def test_intelligence_skips_nonofficial_and_refuses_an_empty_policy(self): + self.controlled_key(official_only=True) + IntelligenceStore.seed(policy(candidates=[candidate("navyai:small"), candidate("openai:large")])) + with self.requests(return_value=upstream(completion())) as send: + response = self.post() + assert response.status_code == 200, response.json + assert send.call_count == 1 + assert send.call_args.kwargs["api_provider"] == "openai" + with patch("routes.intelligence.load_policy", return_value=policy(candidates=[candidate("navyai:small")])): + with self.requests() as send: + response = self.post() + assert response.status_code == 403, response.json + assert response.json["error"]["code"] == "provider_class_not_allowed" + send.assert_not_called() + + def test_forced_preferences_are_captured_before_background_submission(self): + self.controlled_key(forced_provider_preferences={"data_collection": "deny"}) + IntelligenceStore.seed(policy(candidates=[candidate("openrouter:example/model")])) + with self.requests(return_value=upstream(completion())) as send: + response = self.post() + assert response.status_code == 200, response.json + assert json.loads(send.call_args.kwargs["data"])["provider"] == {"data_collection": "deny"} + + def test_managed_submission_pins_the_attested_credential_before_background_dispatch(self): + self.app.config["GEMINI_DATA_TIER"] = "paid" + self.controlled_key(official_only=True) + IntelligenceStore.seed(policy(candidates=[candidate("gemini:gemini-test-model")])) + with patch.dict("os.environ", {"GEMINI_API_KEY": "AIza-attested-synthetic"}), \ + patch.object(self.app_module.AuthService, "get_api_key", return_value="AIza-attested-synthetic"), \ + self.requests(return_value=upstream(completion())) as send: + response = self.post() + assert response.status_code == 200, response.json + assert send.call_count == 1 + headers = send.call_args.kwargs["headers"] + assert headers["x-goog-api-key"] == "AIza-attested-synthetic" + assert not any(name.lower() == "authorization" for name in headers) + + def test_managed_submission_refuses_a_missing_attested_binding(self): + import os + self.app.config["GEMINI_DATA_TIER"] = "paid" + self.controlled_key(official_only=True) + IntelligenceStore.seed(policy(candidates=[candidate("gemini:gemini-test-model")])) + with patch.dict("os.environ"): + os.environ.pop("GEMINI_API_KEY", None) + with self.requests() as send: + response = self.post() + assert response.status_code == 503, response.json + send.assert_not_called() diff --git a/tests/test_key_data_preflight.mjs b/tests/test_key_data_preflight.mjs new file mode 100644 index 00000000..5dfe4894 --- /dev/null +++ b/tests/test_key_data_preflight.mjs @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import { bundleArguments, bundleConfig } from "../scripts/check_worker_bundle.mjs"; +import { checkConfig, checkPrerequisites, checkRelease } from "../scripts/key_data_preflight.mjs"; + +const main = JSON.parse(readFileSync(new URL("../wrangler.jsonc", import.meta.url))); +const knowledge = JSON.parse(readFileSync(new URL("../wrangler.knowledge.jsonc", import.meta.url))); +const commit = "a".repeat(40); +function release(changes = {}) { + const evidence = { + migrations: [{ name: "0043_integration_data_policy.sql" }], + columns: ["official_only", "forced_provider_preferences", "max_stream_cost_microusd"].map(name => ({ name })), + secrets: ["ADMIN_API_KEY", "FLASK_SECRET_KEY", "JWT_SECRET"].map(name => ({ name, type: "secret_text" })), + versions: [{ version_id: "private-version", percentage: 100 }], tag: commit, ...changes, + }; + return { main, knowledge, commit, migrations: ["0043_integration_data_policy.sql"], run: args => { + if (args[0] === "secret") return JSON.stringify(evidence.secrets); + if (args[0] === "deployments") return JSON.stringify({ versions: evidence.versions }); + if (args[0] === "versions") return JSON.stringify({ annotations: { "workers/tag": evidence.tag } }); + if (args[0] === "d1") return JSON.stringify([{ success: true, + results: args.at(-1).includes("PRAGMA") ? evidence.columns : evidence.migrations }]); + throw new Error("Unexpected release probe"); + } }; +} +test("committed release bindings are consistent", () => assert.equal(checkConfig(main, knowledge), "multillm-intelligence")); +test("absent or mismatched configuration refuses deploy admission", () => { + for (const mutation of [config => { delete config.d1_databases; }, config => { delete config.services; }, + config => { config.vars.AUTH_STORAGE_BACKEND = "sql"; }, config => { config.containers = []; }]) { + const config = structuredClone(main); mutation(config); + assert.throws(() => checkConfig(config, knowledge)); + } + assert.throws(() => checkConfig(main, { ...knowledge, d1_databases: [] })); +}); +test("a complete release is admitted without reading secret values", () => assert.doesNotThrow(() => checkRelease(release()))); +test("prerequisites refuse missing main bindings before updating the private Worker", () => { + assert.throws(() => checkPrerequisites(release({ secrets: [] }))); + assert.doesNotThrow(() => checkPrerequisites(release({ tag: "b".repeat(40) }))); +}); +test("missing credentials, schema, migrations or current private version refuse deployment", () => { + for (const changes of [{ secrets: [] }, { columns: [] }, { migrations: [] }, { tag: "b".repeat(40) }, + { versions: [] }, { versions: [{ version_id: "old", percentage: 50 }, { version_id: "new", percentage: 50 }] }]) { + assert.throws(() => checkRelease(release(changes))); + } + assert.throws(() => checkRelease({ ...release(), run: () => { throw new Error("Unavailable evidence"); } })); +}); +test("independent builds cannot upload through the default build without preflight", () => { + assert.match(main.build.command, /^node scripts\/key_data_preflight.mjs --deploy && /); + const ci = readFileSync(new URL("../.github/workflows/ci.yml", import.meta.url), "utf8"); + assert.equal(ci.split("node scripts/check_worker_bundle.mjs --config").length - 1, 2); + for (const mainWorker of [false, true]) assert.ok(bundleArguments("temporary", "output", mainWorker).includes("--dry-run")); + const injected = bundleArguments("temporary", "--dry-run=false", true); + assert.equal(injected.includes("--dry-run=false"), false); + assert.ok(injected.includes("--dry-run")); + const copy = bundleConfig(main); + assert.equal(Object.hasOwn(copy, "build"), false); + assert.deepEqual(copy, Object.fromEntries(Object.entries(main).filter(([name]) => name !== "build"))); + assert.match(main.build.command, /key_data_preflight/); + const manual = readFileSync(new URL("../scripts/deploy.sh", import.meta.url), "utf8"); + const workflow = readFileSync(new URL("../.github/workflows/deploy.yml", import.meta.url), "utf8"); + assert.ok(manual.indexOf("key_data_preflight.mjs --check") < manual.indexOf("migrations apply")); + assert.ok(workflow.indexOf("key_data_preflight.mjs --check") < workflow.indexOf("Apply D1 migrations")); + assert.ok(workflow.indexOf("key_data_preflight.mjs --prepare") < workflow.indexOf("Deploy Knowledge Worker")); + assert.ok(manual.indexOf("key_data_preflight.mjs --prepare") < manual.indexOf("wrangler deploy --config wrangler.knowledge.jsonc")); +}); diff --git a/tests/test_kimi_code_transport.py b/tests/test_kimi_code_transport.py index 523d473e..181d49fc 100644 --- a/tests/test_kimi_code_transport.py +++ b/tests/test_kimi_code_transport.py @@ -9,6 +9,12 @@ import requests + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class KimiCodeNativeRouteTest(unittest.TestCase): def setUp(self): self.original_env = os.environ.copy() diff --git a/tests/test_knowledge_alexandria.mjs b/tests/test_knowledge_alexandria.mjs index b2f6c9f1..6243cc0e 100644 --- a/tests/test_knowledge_alexandria.mjs +++ b/tests/test_knowledge_alexandria.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { dispatchKnowledge } from "../worker/knowledge/service.mjs"; import { KnowledgeAuthority } from "../worker/knowledge/authority.mjs"; import { defaultPolicy, validatePolicy } from "../worker/knowledge/policy.mjs"; diff --git a/tests/test_knowledge_authority.mjs b/tests/test_knowledge_authority.mjs index f60948cc..7a01d716 100644 --- a/tests/test_knowledge_authority.mjs +++ b/tests/test_knowledge_authority.mjs @@ -2,9 +2,9 @@ import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { defaultPolicy, validatePolicy } from "../worker/knowledge/policy.mjs"; import { createArtifact, normalizeSourceText, packEvidence, validateChunk, versionEvidence } from "../worker/knowledge/evidence.mjs"; import { parseQuery, publicHost, publicUrl } from "../worker/knowledge/contracts.mjs"; @@ -22,7 +22,7 @@ const bundled = await build({ stdin: { resolveDir: process.cwd(), sourcefile: "k } } export default { fetch(request, env) { return env.CATALOGUE.get(env.CATALOGUE.idFromName('personal')).fetch(request); } }; -` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers"], write: false }); +` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"], write: false }); function enabledPolicy(limit = 10) { const { revision, ...policy } = defaultPolicy(); @@ -34,7 +34,7 @@ function enabledPolicy(limit = 10) { async function fixture(t) { const directory = await mkdtemp(join(tmpdir(), "knowledge-authority-")); - const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", + const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], compatibilityDate: "2026-07-28", script: bundled.outputFiles[0].text, resourcePersistencePath: directory, durableObjects: { CATALOGUE: { className: "Catalogue", useSQLite: true } } })); let mf = create(); @@ -71,12 +71,13 @@ test("SQLite Durable Object serializes concurrent reservations and survives reop }))); assert.equal(attempts.filter(item => item.status === 200).length, 5); assert.equal(attempts.filter(item => item.status === 429).length, 20); + const acceptedId = `query-${attempts.findIndex(item => item.status === 200)}`; await f.reopen(); const usage = (await f.call("snapshot")).result.usage.find(item => item.provider === "firecrawl"); assert.equal(usage.total, 5); assert.equal(usage.pending, 5); - assert.equal((await f.call("reserve", { operation_id: "query-0", provider: "firecrawl", background: false })).result.replay, true); - assert.equal((await f.call("reserve", { operation_id: "query-0", provider: "exa", background: false })).status, 409); + assert.equal((await f.call("reserve", { operation_id: acceptedId, provider: "firecrawl", background: false })).result.replay, true); + assert.equal((await f.call("reserve", { operation_id: acceptedId, provider: "exa", background: false })).status, 409); }); test("credential failover persists across SQLite restarts and concurrent selection", async t => { @@ -264,3 +265,24 @@ test("product-site learning and overrides survive Durable Object reopen and rebu assert.ok(rebuilt.sites["python.org"].pinned); assert.deepEqual(rebuilt.blocked, overrides.blocked); }); + + +test("background refresh and reconciliation retain the source account identity", async t => { + const f = await fixture(t); + const owner = { authenticated: true, keyId: "source-key" }; + const other = { authenticated: true, keyId: "admin" }; + const source = (await f.call("source.create", { url: "https://flask.palletsprojects.com/en/3.1.3/api/", + product: "flask", version: "3.1.3", dispatch_principal: owner })).result; + const job = (await f.call("job.enqueue", { source_id: source.id, dispatch_principal: other })).result; + assert.deepEqual(job.dispatch_principal, owner); + const replay = (await f.call("job.enqueue", { source_id: source.id, dispatch_principal: other })).result; + assert.equal(replay.id, job.id); + assert.deepEqual(replay.dispatch_principal, owner); + await f.call("job.cancel", { id: job.id }); + await f.reopen(); + const replacement = (await f.call("job.enqueue", { source_id: source.id, dispatch_principal: other })).result; + assert.notEqual(replacement.id, job.id); + assert.deepEqual(replacement.dispatch_principal, owner); + const persisted = (await f.call("job.get", { id: replacement.id })).result; + assert.deepEqual(persisted.source.dispatch_principal, owner); +}); diff --git a/tests/test_knowledge_corpus.mjs b/tests/test_knowledge_corpus.mjs index c9c2c5d7..6ccc7926 100644 --- a/tests/test_knowledge_corpus.mjs +++ b/tests/test_knowledge_corpus.mjs @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import test from "./helpers/policy_test.mjs"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { KnowledgeCorpus } from "../worker/knowledge/corpus.mjs"; +import { providerBinding } from "./helpers/provider_binding.mjs"; const text = "# Flask 3.1\n\nRequest limits support café and 東京.\n"; const digest = value => createHash("sha256").update(value).digest("hex"); @@ -32,7 +33,7 @@ function fixture() { return bytes ? { size: bytes.byteLength, body: new Response(bytes).body } : null; }, }; - const index = { + const index = providerBinding({ async search(request) { searches.push(request); return { chunks: [{ text, score: 0.8, item: { key: artifact().index_key } }] }; @@ -57,7 +58,7 @@ function fixture() { }; }, }, - }; + }); return { corpus: new KnowledgeCorpus({ KNOWLEDGE_SNAPSHOTS: bucket, KNOWLEDGE_INDEX: index }), objects, uploads, searches, lists, bucket, index, @@ -84,7 +85,7 @@ test("snapshots are immutable, hash checked and readable across corpus instances test("the real local R2 binding honors immutable snapshot preconditions", async t => { const runtime = new Miniflare(convertV4MiniflareOptions({ - cf: false, modules: true, script: "export default { fetch() { return new Response('ok'); } };", + cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], script: "export default { fetch() { return new Response('ok'); } };", r2Buckets: { KNOWLEDGE_SNAPSHOTS: "knowledge-snapshot-test" }, })); t.after(() => runtime.dispose()); @@ -138,14 +139,14 @@ test("AI binding search preserves chunk identity and scores without calling inst const f = fixture(); const calls = []; const corpus = new KnowledgeCorpus({ KNOWLEDGE_INDEX: f.index, KNOWLEDGE_SEARCH_INSTANCE: "knowledge-test", - KNOWLEDGE_SEARCH_AI: { autorag(name) { + KNOWLEDGE_SEARCH_AI: providerBinding({ autorag(name) { assert.equal(name, "knowledge-test"); return { async search(query) { calls.push(query); return { data: [{ filename: artifact().index_key, score: 0.8, content: [{ type: "text", text, score: 0.9 }, { type: "text", text: "東京" }] }] }; } }; - } }, + } }), }); assert.deepEqual(await corpus.search({ query: "request limits", product: "flask" }), [ { text, index_key: artifact().index_key, score: 0.9 }, @@ -163,11 +164,11 @@ test("AI binding failures never fall back or retry and malformed responses fail let calls = 0; let response; const corpus = new KnowledgeCorpus({ KNOWLEDGE_INDEX: f.index, KNOWLEDGE_SEARCH_INSTANCE: "knowledge-test", - KNOWLEDGE_SEARCH_AI: { autorag() { return { async search() { + KNOWLEDGE_SEARCH_AI: providerBinding({ autorag() { return { async search() { calls += 1; if (response instanceof Error) throw response; return response; - } }; } }, + } }; } }), }); response = new Error("Invalid ctx.props: missing accountId or accountTag"); await assert.rejects(corpus.search({ query: "limits" }), /missing accountId/); diff --git a/tests/test_knowledge_edge.mjs b/tests/test_knowledge_edge.mjs index 182dbe79..55c1fd1e 100644 --- a/tests/test_knowledge_edge.mjs +++ b/tests/test_knowledge_edge.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { generateIntegrationCredential, hashIntegrationKey } from "../scripts/intelligence_operator.mjs"; import { handleKnowledgeEdgeRequest, isKnowledgeEdgePath } from "../worker/knowledge-edge.mjs"; @@ -271,7 +271,7 @@ test("dashboard accounts stored in D1 are verified at the edge with their Knowle return { key, row: { username, api_key_hash: await hashIntegrationKey(key, "saltsaltsalt1234"), api_key_prefix: `mllm_${key.slice(0, 8)}`, scopes, is_admin: 0, created_at: "2026-09-24T00:00:00+00:00", last_login: null, last_used_at: null, last_used_ip: null, created_by: "admin", rotated_at: null, revoked_at: null, daily_budget_usd: null, monthly_budget_usd: null, - allowed_models: null, allowed_ips: null, expires_at: null, ...changes } }; + allowed_models: null, allowed_ips: null, expires_at: null, official_only: null, forced_provider_preferences: null, ...changes } }; }; const reader = await account("reader", "knowledge:read"); const chat = await account("chatter", "chat,models"); diff --git a/tests/test_knowledge_handoffs.mjs b/tests/test_knowledge_handoffs.mjs index 44fe2c4c..3efafd8c 100644 --- a/tests/test_knowledge_handoffs.mjs +++ b/tests/test_knowledge_handoffs.mjs @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { DatabaseSync } from "node:sqlite"; import { readFileSync } from "node:fs"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -169,9 +169,9 @@ test("real SQLite handoff Durable Objects isolate principals and survive restart const bundled = await build({ stdin: { resolveDir: process.cwd(), contents: ` export { KnowledgeHandoffs } from "./worker/knowledge/index.mjs"; export default { fetch(request, env) { return env.HANDOFFS.get(env.HANDOFFS.idFromName(new URL(request.url).searchParams.get("principal"))).fetch(request); } }; - ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers"], write: false }); + ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"], write: false }); const directory = await mkdtemp(join(tmpdir(), "knowledge-handoffs-")); - const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", + const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], compatibilityDate: "2026-07-28", script: bundled.outputFiles[0].text, resourcePersistencePath: directory, durableObjects: { HANDOFFS: { className: "KnowledgeHandoffs", useSQLite: true } } })); let mf = create(); t.after(async () => { await mf.dispose(); await rm(directory, { recursive: true, force: true }); }); diff --git a/tests/test_knowledge_ingestion.mjs b/tests/test_knowledge_ingestion.mjs index 8285bbbe..2734c218 100644 --- a/tests/test_knowledge_ingestion.mjs +++ b/tests/test_knowledge_ingestion.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { KnowledgeAuthority } from "../worker/knowledge/authority.mjs"; import { defaultPolicy } from "../worker/knowledge/policy.mjs"; import { metered } from "../worker/knowledge/operations.mjs"; diff --git a/tests/test_knowledge_keys.mjs b/tests/test_knowledge_keys.mjs index d0d9c482..ad48f955 100644 --- a/tests/test_knowledge_keys.mjs +++ b/tests/test_knowledge_keys.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { configuredKeys, MAX_KEYS } from "../worker/knowledge/providers/keys.mjs"; import { upstreamError } from "../worker/knowledge/providers/errors.mjs"; import { requestJSON } from "../worker/knowledge/providers/transport.mjs"; diff --git a/tests/test_knowledge_ledger.mjs b/tests/test_knowledge_ledger.mjs index a39633bf..391fdb96 100644 --- a/tests/test_knowledge_ledger.mjs +++ b/tests/test_knowledge_ledger.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { KnowledgeAuthority } from "../worker/knowledge/authority.mjs"; import { fixture } from "./knowledge_fixture.mjs"; diff --git a/tests/test_knowledge_memo_latency.mjs b/tests/test_knowledge_memo_latency.mjs index 1e984da1..f944ea3e 100644 --- a/tests/test_knowledge_memo_latency.mjs +++ b/tests/test_knowledge_memo_latency.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { DatabaseSync } from "node:sqlite"; import { setTimeout as delay } from "node:timers/promises"; import { MemoStore } from "../worker/knowledge/memo-store.mjs"; diff --git a/tests/test_knowledge_memos.mjs b/tests/test_knowledge_memos.mjs index f7c83336..afc8c032 100644 --- a/tests/test_knowledge_memos.mjs +++ b/tests/test_knowledge_memos.mjs @@ -1,8 +1,9 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import { providerBinding } from "./helpers/provider_binding.mjs"; +import test from "./helpers/policy_test.mjs"; import { DatabaseSync } from "node:sqlite"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -320,7 +321,7 @@ test("embedding or memo backend errors degrade to ordinary retrieval", async t = test("Workers AI binding receives bge-m3 request and storage uses waitUntil", async t => { const f = await setup(t); let called; - f.env.KNOWLEDGE_SEARCH_AI = { run: async (...args) => { called = args; return { data: [[0.5, 1, -1]] }; } }; + f.env.KNOWLEDGE_SEARCH_AI = providerBinding({ run: async (...args) => { called = args; return { data: [[0.5, 1, -1]] }; } }); const tasks = []; await f.run(f.query, { embed: undefined, waitUntil: promise => tasks.push(promise) }); await Promise.all(tasks); @@ -395,9 +396,9 @@ test("real KnowledgeMemos SQLite Durable Object survives restart with hit counts const bundled = await build({ stdin: { resolveDir: process.cwd(), contents: ` export { KnowledgeMemos } from './worker/knowledge/index.mjs'; export default { fetch(request, env) { return env.MEMOS.get(env.MEMOS.idFromName('personal')).fetch(request); } }; - ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers"], write: false }); + ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"], write: false }); const directory = await mkdtemp(join(tmpdir(), "knowledge-memos-")); - const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", + const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], compatibilityDate: "2026-07-28", script: bundled.outputFiles[0].text, resourcePersistencePath: directory, durableObjects: { MEMOS: { className: "KnowledgeMemos", useSQLite: true } } })); let mf = create(); t.after(async () => { await mf.dispose(); await rm(directory, { recursive: true, force: true }); }); diff --git a/tests/test_knowledge_native.mjs b/tests/test_knowledge_native.mjs index 90f40051..4b244928 100644 --- a/tests/test_knowledge_native.mjs +++ b/tests/test_knowledge_native.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { dispatchNative, NATIVE_OPERATIONS, nativeUnits } from "../worker/knowledge/native.mjs"; import { dispatchKnowledge } from "../worker/knowledge/service.mjs"; diff --git a/tests/test_knowledge_product_site_boundaries.mjs b/tests/test_knowledge_product_site_boundaries.mjs index f6838eb5..9258403f 100644 --- a/tests/test_knowledge_product_site_boundaries.mjs +++ b/tests/test_knowledge_product_site_boundaries.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { createArtifact, dropForeignProviderSections, site } from "../worker/knowledge/evidence.mjs"; import { parseProductSites } from "../worker/knowledge/product-sites.mjs"; import { filterProviderSites } from "../worker/knowledge/provider-site-filter.mjs"; diff --git a/tests/test_knowledge_product_site_latency.mjs b/tests/test_knowledge_product_site_latency.mjs index 122b7099..8054433c 100644 --- a/tests/test_knowledge_product_site_latency.mjs +++ b/tests/test_knowledge_product_site_latency.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { retrieveKnowledge } from "../worker/knowledge/retrieval.mjs"; import { fixture, principal, request } from "./knowledge_fixture.mjs"; diff --git a/tests/test_knowledge_product_site_storage.mjs b/tests/test_knowledge_product_site_storage.mjs index c10142b9..f4752714 100644 --- a/tests/test_knowledge_product_site_storage.mjs +++ b/tests/test_knowledge_product_site_storage.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { fixture, seedProductSitePages } from "./knowledge_fixture.mjs"; import { PAGE_LIMIT } from "../worker/knowledge/product-sites.mjs"; import { lruKey, pageKey } from "../worker/knowledge/product-site-identities.mjs"; diff --git a/tests/test_knowledge_product_sites.mjs b/tests/test_knowledge_product_sites.mjs index 7121d406..daf7f1b5 100644 --- a/tests/test_knowledge_product_sites.mjs +++ b/tests/test_knowledge_product_sites.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { createArtifact, dropForeignProviderSections, site } from "../worker/knowledge/evidence.mjs"; import { parseProductSites, SITE_LIMIT, PAGE_LIMIT } from "../worker/knowledge/product-sites.mjs"; import { filterProviderSites } from "../worker/knowledge/provider-site-filter.mjs"; diff --git a/tests/test_knowledge_provider_runtime.mjs b/tests/test_knowledge_provider_runtime.mjs index 709488e8..60e99733 100644 --- a/tests/test_knowledge_provider_runtime.mjs +++ b/tests/test_knowledge_provider_runtime.mjs @@ -1,23 +1,25 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; async function runtime(t, outboundService) { const bundle = await build({ stdin: { resolveDir: process.cwd(), contents: ` + import { withDispatchPrincipal } from "./worker/provider-dispatch.mjs"; + import { policyStore } from "./tests/helpers/dispatch_fixture.mjs"; import { requestJSON, jsonPost } from "./worker/knowledge/providers/transport.mjs"; export default { async fetch() { try { - const result = await requestJSON("firecrawl", "scrape", "https://api.firecrawl.dev/v2/scrape", + const result = await withDispatchPrincipal({ INTELLIGENCE_DB: policyStore() }, { authenticated: true, keyId: "fixture-reader" }, () => requestJSON("firecrawl", "scrape", "https://api.firecrawl.dev/v2/scrape", jsonPost({ url: "https://docs.python.org/" }), { env: { FIRECRAWL_API_KEY: "synthetic-key" }, fetchImpl: fetch, invoke: (_provider, _operation, callback) => callback(), - }); + })); return Response.json(result); } catch (error) { return Response.json({ code: error.code }, { status: 502 }); } }}; - ` }, bundle: true, write: false, format: "esm", platform: "neutral" }); - const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, script: bundle.outputFiles[0].text, + ` }, bundle: true, write: false, format: "esm", platform: "neutral", external: ["node:*"] }); + const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], script: bundle.outputFiles[0].text, compatibilityDate: "2026-07-28", outboundService })); t.after(() => mf.dispose()); return mf; diff --git a/tests/test_knowledge_providers.mjs b/tests/test_knowledge_providers.mjs index ad25b717..77e868a8 100644 --- a/tests/test_knowledge_providers.mjs +++ b/tests/test_knowledge_providers.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { PROVIDERS, providerStatus, ProviderError, retrieve } from "../worker/knowledge/providers/index.mjs"; import { sourceLinks, sourceOperation, sourceURL } from "../worker/knowledge/providers/source-policy.mjs"; diff --git a/tests/test_knowledge_retrieval.mjs b/tests/test_knowledge_retrieval.mjs index c77f30d8..def88168 100644 --- a/tests/test_knowledge_retrieval.mjs +++ b/tests/test_knowledge_retrieval.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { setTimeout as delay } from "node:timers/promises"; import { retrieveKnowledge } from "../worker/knowledge/retrieval.mjs"; import { retrieve } from "../worker/knowledge/providers/index.mjs"; diff --git a/tests/test_knowledge_routes.py b/tests/test_knowledge_routes.py index 2f5bcb33..1a3e2b44 100644 --- a/tests/test_knowledge_routes.py +++ b/tests/test_knowledge_routes.py @@ -344,15 +344,18 @@ def __exit__(self, *_args): def test_private_transport_uses_one_fixed_submission(monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda username: {"username": username, + "data_policy": {"version": 1, "official_only": None, "forced_provider_preferences": None}}) monkeypatch.setenv("KNOWLEDGE_SERVICE_ENABLED", "true") with patch.object(knowledge_client.requests, "Session") as factory: session = factory.return_value.__enter__.return_value - session.post.return_value = FakeResponse({"version": 1, "result": {"status": "ok"}}) + session.request.return_value = FakeResponse({"version": 1, "result": {"status": "ok"}}) result = knowledge_client.dispatch("context", {"username": "reader", "scopes": ["knowledge:read", "chat"]}, {"query": "limits"}) assert result == {"status": "ok"} - assert session.post.call_count == 1 - assert session.post.call_args.args == ("http://knowledge.internal/v1/dispatch",) - arguments = session.post.call_args.kwargs + assert session.request.call_count == 1 + assert session.request.call_args.kwargs["method"] == "POST" + assert session.request.call_args.kwargs["url"] == "http://knowledge.internal/v1/dispatch" + arguments = session.request.call_args.kwargs assert arguments["allow_redirects"] is False assert session.trust_env is False assert "Authorization" not in arguments["headers"] @@ -451,13 +454,15 @@ def test_alexandria_mcp_surfaces_unknown_cost_as_tool_error(app, keys): def test_alexandria_private_operation_is_allowlisted(monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda username: {"username": username, + "data_policy": {"version": 1, "official_only": None, "forced_provider_preferences": None}}) monkeypatch.setenv("KNOWLEDGE_SERVICE_ENABLED", "true") with patch.object(knowledge_client.requests, "Session") as factory: session = factory.return_value.__enter__.return_value - session.post.return_value = FakeResponse({"version": 1, "result": {"tools": [], "cost": {"credits": 0, "state": "confirmed"}}}) + session.request.return_value = FakeResponse({"version": 1, "result": {"tools": [], "cost": {"credits": 0, "state": "confirmed"}}}) result = knowledge_client.dispatch("alexandria.search", {"username": "reader", "scopes": ["knowledge:read"]}, {"query": "podcasts"}) assert result["cost"]["credits"] == 0 - assert json.loads(session.post.call_args.kwargs["data"])["operation"] == "alexandria.search" + assert json.loads(session.request.call_args.kwargs["data"])["operation"] == "alexandria.search" def test_provider_tools_forward_native_arguments_over_mcp_and_rest(app, keys): diff --git a/tests/test_knowledge_secret_firewall.mjs b/tests/test_knowledge_secret_firewall.mjs index 449ec4ac..3eb33b22 100644 --- a/tests/test_knowledge_secret_firewall.mjs +++ b/tests/test_knowledge_secret_firewall.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { readFileSync } from "node:fs"; import { handleKnowledgeEdgeRequest } from "../worker/knowledge-edge.mjs"; import { dispatchKnowledge } from "../worker/knowledge/service.mjs"; diff --git a/tests/test_knowledge_service.mjs b/tests/test_knowledge_service.mjs index 228247b8..0f2ef6a9 100644 --- a/tests/test_knowledge_service.mjs +++ b/tests/test_knowledge_service.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { dispatchKnowledge, maintainKnowledge, scheduleSource } from "../worker/knowledge/service.mjs"; import { KnowledgeAuthority, RECONCILE_BATCH } from "../worker/knowledge/authority.mjs"; import { createArtifact } from "../worker/knowledge/evidence.mjs"; @@ -300,11 +300,11 @@ test("Container outbound forwarding accepts only its fixed private target and st return Response.json({ version: 1, result: { ready: false } }); } } }; const body = JSON.stringify(envelope("status")); - const options = { method: "POST", headers: { "content-type": "application/json", "authorization": "Bearer synthetic" }, body }; + const options = { method: "POST", headers: { "content-type": "application/json", "authorization": "Bearer synthetic", "X-MultiLLM-Dispatch-Principal": JSON.stringify({ version: 1, username: manager.id }) }, body }; const result = await handleKnowledgeOutbound(new Request("http://knowledge.internal/v1/dispatch", options), env); assert.equal(result.status, 200); assert.equal(requests.length, 1); - assert.deepEqual(requests[0].options.headers, { "content-type": "application/json" }); + assert.deepEqual(requests[0].options.headers, { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": JSON.stringify({ version: 1, username: manager.id }) }); for (const target of ["http://knowledge.internal/other", "https://knowledge.internal/v1/dispatch", "http://other.internal/v1/dispatch", "http://knowledge.internal/v1/dispatch?url=anything"]) { assert.equal((await handleKnowledgeOutbound(new Request(target, options), env)).status, 400); } @@ -322,11 +322,11 @@ test("Container outbound forwarding accepts only its fixed private target and st test("bundled private Worker serves disabled setup through the real Durable Object binding", async t => { const bundle = await build({ entryPoints: ["worker/knowledge/index.mjs"], bundle: true, write: false, - format: "esm", platform: "neutral", external: ["cloudflare:workers"] }); - const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", script: bundle.outputFiles[0].text, + format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"] }); + const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", compatibilityFlags: ["nodejs_compat"], script: bundle.outputFiles[0].text, durableObjects: { KNOWLEDGE_AUTHORITY: { className: "KnowledgeCatalogue", useSQLite: true } } })); t.after(() => mf.dispose()); - const options = { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(envelope("status")) }; + const options = { method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": JSON.stringify({ version: 1, username: manager.id }) }, body: JSON.stringify(envelope("status")) }; const result = await mf.dispatchFetch("http://knowledge.internal/v1/dispatch", options); assert.equal(result.status, 200); assert.equal(result.headers.get("cache-control"), "no-store"); diff --git a/tests/test_knowledge_skills.mjs b/tests/test_knowledge_skills.mjs index da4046b0..21b7da8f 100644 --- a/tests/test_knowledge_skills.mjs +++ b/tests/test_knowledge_skills.mjs @@ -1,11 +1,11 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { SkillsStore } from "../worker/knowledge/skills-store.mjs"; import { SkillsIndex, terms } from "../worker/knowledge/skills-index.mjs"; import { parseFind, parseGet, parseSync, validateSkill, SKILLS_LIMIT, SYNC_REQUEST_BYTES } from "../worker/knowledge/skills-validation.mjs"; @@ -149,14 +149,14 @@ test("real Skills Durable Object survives restart with R2 files and helpful coun const bundled = await build({ stdin: { resolveDir: process.cwd(), contents: ` export { KnowledgeSkills } from './worker/knowledge/index.mjs'; export default { fetch(request, env) { return env.KNOWLEDGE_SKILLS.get(env.KNOWLEDGE_SKILLS.idFromName('personal')).fetch(request); } }; - ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers"], write: false }); + ` }, bundle: true, format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"], write: false }); const directory = await mkdtemp(join(tmpdir(), "knowledge-skills-")); - const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityDate: "2026-07-28", script: bundled.outputFiles[0].text, + const create = () => new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], compatibilityDate: "2026-07-28", script: bundled.outputFiles[0].text, resourcePersistencePath: directory, durableObjects: { KNOWLEDGE_SKILLS: { className: "KnowledgeSkills", useSQLite: true } }, r2Buckets: ["KNOWLEDGE_SNAPSHOTS"] })); let mf = create(); t.after(async () => { await mf.dispose(); await rm(directory, { recursive: true, force: true }); }); const call = async (operation, payload) => { - const response = await mf.dispatchFetch("http://skills.internal/dispatch", { method: "POST", headers: { "content-type": "application/json" }, + const response = await mf.dispatchFetch("http://skills.internal/dispatch", { method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"fixture"}' }, body: JSON.stringify({ operation, payload, principal: "synthetic-principal" }) }); assert.equal(response.status, 200); return (await response.json()).result; @@ -394,7 +394,7 @@ test("hybrid semantic confidence requires explicit calibration and fast mode nev test("8 MiB sync ingress and outbound boundary accept exact size and reject one extra byte", async () => { assert.equal(SYNC_REQUEST_BYTES, 8 * 1024 * 1024); const env = { ADMIN_API_KEY: "synthetic-skills-boundary", ADMIN_USERNAME: "operator", KNOWLEDGE_SERVICE: { fetch: async () => Response.json({ version: 1, result: { results: [] } }) } }; - const request = (url, body) => new Request(url, { method: "POST", headers: { authorization: "Bearer synthetic-skills-boundary", "content-type": "application/json" }, body }); + const request = (url, body) => new Request(url, { method: "POST", headers: { authorization: "Bearer synthetic-skills-boundary", "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"fixture"}' }, body }); const raw = JSON.stringify({ skills: [] }); for (const [bytes, expected] of [[SYNC_REQUEST_BYTES, 200], [SYNC_REQUEST_BYTES + 1, 413]]) { const response = await handleKnowledgeEdgeRequest(request("https://gateway.example/v1/knowledge/skills", raw.padEnd(bytes)), env); diff --git a/tests/test_knowledge_skills_import.mjs b/tests/test_knowledge_skills_import.mjs index b6ec18dc..51b98c5f 100644 --- a/tests/test_knowledge_skills_import.mjs +++ b/tests/test_knowledge_skills_import.mjs @@ -1,5 +1,6 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { DatabaseSync } from "node:sqlite"; import { digest } from "../worker/knowledge/evidence.mjs"; import { dispatchKnowledge } from "../worker/knowledge/service.mjs"; @@ -23,7 +24,7 @@ function library(t, clock) { catch (error) { db.exec("ROLLBACK"); throw error; } } }; const files = new Map(); - const env = { KNOWLEDGE_SNAPSHOTS: { put: async (key, bytes) => files.set(key, bytes), delete: async () => {}, get: async key => { + const env = { INTELLIGENCE_DB: policyStore(), KNOWLEDGE_SNAPSHOTS: { put: async (key, bytes) => files.set(key, bytes), delete: async () => {}, get: async key => { const bytes = files.get(key); return bytes && { arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }; } } }; return { env, store: new SkillsStore(storage, env, clock ? { clock } : {}) }; @@ -291,3 +292,21 @@ test("the generated MCP contract matches the Worker's import flags, roots and sc const schema = tool("knowledge_skills_import").definition.inputSchema.properties; for (const [field, value] of [["commit", SHA1], ["version", "1.0.0-beta+2"], ["clawhub", "awspace/pdf"]]) assert.match(value, new RegExp(schema[field].pattern), field); }); + + +test("approved reimport binds legacy watches and preserves the importing account", async t => { + const { env, store } = library(t); + const repo = github({ head: SHA1, commits: { [SHA1]: { tree: "t1", files: { "SKILL.md": SKILL } } } }); + const payload = { repository: "acme/skills", path: "skills/pg", commit: SHA1 }; + const options = { skills: store, fetch: repo.fetch }; + await submit(env, "skills.import", payload, options); + assert.deepEqual(store.read("pg-tools").origin.dispatch_principal, { authenticated: true, keyId: "synthetic-principal" }); + const legacy = store.read("pg-tools"); + delete legacy.origin.dispatch_principal; + store.sql.exec("UPDATE skills SET record = ? WHERE skill_id = ?", JSON.stringify(legacy), "pg-tools"); + assert.equal((await submit(env, "skills.import", payload, options)).status, "unchanged"); + assert.deepEqual(store.read("pg-tools").origin.dispatch_principal, { authenticated: true, keyId: "synthetic-principal" }); + await dispatchKnowledge(env, { version: 1, operation: "skills.import", payload, + principal: { id: "other-principal", scopes: MANAGE } }, options); + assert.deepEqual(store.read("pg-tools").origin.dispatch_principal, { authenticated: true, keyId: "synthetic-principal" }); +}); diff --git a/tests/test_knowledge_skills_market.mjs b/tests/test_knowledge_skills_market.mjs index 64821681..65680f27 100644 --- a/tests/test_knowledge_skills_market.mjs +++ b/tests/test_knowledge_skills_market.mjs @@ -1,5 +1,6 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { DatabaseSync } from "node:sqlite"; import { errorReply } from "../worker/knowledge/contracts.mjs"; import { digest } from "../worker/knowledge/evidence.mjs"; @@ -60,7 +61,7 @@ function library(t) { catch (error) { db.exec("ROLLBACK"); throw error; } } }; const files = new Map(); - const env = { KNOWLEDGE_SNAPSHOTS: { put: async (key, bytes) => files.set(key, bytes), delete: async () => {}, get: async key => { + const env = { INTELLIGENCE_DB: policyStore(), KNOWLEDGE_SNAPSHOTS: { put: async (key, bytes) => files.set(key, bytes), delete: async () => {}, get: async key => { const bytes = files.get(key); return bytes && { arrayBuffer: async () => bytes.buffer }; } } }; return { env, store: new SkillsStore(storage, env) }; @@ -72,7 +73,7 @@ async function seed(store) { description: "Postgres patterns for this team", files: [{ path: "SKILL.md", content, sha256: await digest(content) }] }] }); } -const submit = (env, operation, payload, options) => dispatchKnowledge(env, { version: 1, operation, +const submit = (env, operation, payload, options) => dispatchKnowledge({ INTELLIGENCE_DB: policyStore(), ...env }, { version: 1, operation, principal: { id: "synthetic-principal", scopes: ["knowledge:read"] }, payload }, options); test("discover merges marketplaces by repository, withholds flagged skills and reports every source", async t => { diff --git a/tests/test_knowledge_ui.mjs b/tests/test_knowledge_ui.mjs index 16742416..e097a920 100644 --- a/tests/test_knowledge_ui.mjs +++ b/tests/test_knowledge_ui.mjs @@ -1,5 +1,6 @@ +import { clientNetwork } from "./helpers/network_fixture.mjs"; import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { api, queryPayload, sourcePayload } from "../static/js/knowledge/api.mjs"; import { policyPayload, renderPolicy } from "../static/js/knowledge/policy.mjs"; import { safeUrl, renderEvidence, renderSources, renderJobs, renderReadiness } from "../static/js/knowledge/render.mjs"; @@ -128,7 +129,7 @@ test("admin transport sends CSRF on mutations without client credentials", async dom(); let call; globalThis.fetch = async (url, options) => { call = { url, options }; return Response.json({ id: "s1" }); }; - await api("sources", { method: "POST", body: { product: "flask" } }); + await clientNetwork("/admin/knowledge/sources", () => api("sources", { method: "POST", body: { product: "flask" } })); assert.equal(call.url, "/admin/knowledge/sources"); assert.equal(call.options.headers.get("X-CSRFToken"), "synthetic-csrf"); assert.equal(call.options.credentials, "same-origin"); @@ -140,7 +141,7 @@ test("admin transport reports a conflict and never retries automatically", async dom(); let calls = 0; globalThis.fetch = async () => { calls += 1; return Response.json({ error: { code: "revision_conflict", message: "Reload the current policy." } }, { status: 409 }); }; - await assert.rejects(api("policy", { method: "PUT", body: {} }), /Reload the current policy/); + await assert.rejects(clientNetwork("/admin/knowledge/policy", () => api("policy", { method: "PUT", body: {} })), /Reload the current policy/); assert.equal(calls, 1); }); diff --git a/tests/test_knowledge_workflow_runtime.mjs b/tests/test_knowledge_workflow_runtime.mjs index 3a320d29..47ae1233 100644 --- a/tests/test_knowledge_workflow_runtime.mjs +++ b/tests/test_knowledge_workflow_runtime.mjs @@ -1,17 +1,19 @@ import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { setTimeout } from "node:timers/promises"; import { build } from "esbuild"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; test("native Workflow accepts ingestion step contracts and publishes once", async t => { const bundle = await build({ stdin: { resolveDir: process.cwd(), contents: ` + import { withDispatchPrincipal } from "./worker/provider-dispatch.mjs"; + import { policyStore } from "./tests/helpers/dispatch_fixture.mjs"; import worker, { KnowledgeCatalogue } from "./worker/knowledge/index.mjs"; import { WorkflowEntrypoint } from "cloudflare:workers"; import { KnowledgeCorpus } from "./worker/knowledge/corpus.mjs"; import { runIngestion } from "./worker/knowledge/ingestion.mjs"; export { KnowledgeCatalogue }; - export default worker; + export default { fetch(request, env, ctx) { return worker.fetch(request, { ...env, INTELLIGENCE_DB: policyStore() }, ctx); } }; export class TestIngestion extends WorkflowEntrypoint { async run(event, step) { const snapshots = new KnowledgeCorpus(this.env); @@ -26,12 +28,12 @@ test("native Workflow accepts ingestion step contracts and publishes once", asyn }, async verifySearchable() { return true; }, }; - return runIngestion(this.env, step, event.payload.job_id, { corpus }); + return withDispatchPrincipal({ ...this.env, INTELLIGENCE_DB: policyStore() }, event.payload.dispatch_principal, () => runIngestion(this.env, step, event.payload.job_id, { corpus })); } } - ` }, bundle: true, write: false, format: "esm", platform: "neutral", external: ["cloudflare:workers"] }); + ` }, bundle: true, write: false, format: "esm", platform: "neutral", external: ["cloudflare:workers", "node:*"] }); let acquisitions = 0; - const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, script: bundle.outputFiles[0].text, + const mf = new Miniflare(convertV4MiniflareOptions({ cf: false, modules: true, compatibilityFlags: ["nodejs_compat"], script: bundle.outputFiles[0].text, compatibilityDate: "2026-07-28", bindings: { FIRECRAWL_API_KEY: "synthetic-key" }, durableObjects: { KNOWLEDGE_AUTHORITY: { className: "KnowledgeCatalogue", useSQLite: true } }, workflows: { KNOWLEDGE_INGESTION: { name: "test-ingestion", className: "TestIngestion" } }, @@ -46,7 +48,7 @@ test("native Workflow accepts ingestion step contracts and publishes once", asyn t.after(() => mf.dispose()); async function dispatch(operation, payload = {}) { const response = await mf.dispatchFetch("http://knowledge.internal/v1/dispatch", { - method: "POST", headers: { "content-type": "application/json" }, + method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"fixture"}' }, body: JSON.stringify({ version: 1, operation, principal: { id: "fixture", scopes: ["knowledge:manage"] }, payload }), }); const body = await response.json(); diff --git a/tests/test_mcp_contract_drift_worker.mjs b/tests/test_mcp_contract_drift_worker.mjs index 372dd5a2..e11f5540 100644 --- a/tests/test_mcp_contract_drift_worker.mjs +++ b/tests/test_mcp_contract_drift_worker.mjs @@ -1,9 +1,16 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { canonicalContract, canonicalValue, contractDigest, catalogueDigest, discoveryContract, digestsEnabled, checkContractPin } from "../worker/knowledge/contract-drift.mjs"; import { dispatchNative } from "../worker/knowledge/native.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { generateIntegrationCredential } from "../scripts/intelligence_operator.mjs"; import { fixture, principal } from "./knowledge_fixture.mjs"; diff --git a/tests/test_media_files_worker.mjs b/tests/test_media_files_worker.mjs index 44a45d1e..3846725d 100644 --- a/tests/test_media_files_worker.mjs +++ b/tests/test_media_files_worker.mjs @@ -1,3 +1,4 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; import { Buffer } from "node:buffer"; import test from "node:test"; @@ -45,7 +46,8 @@ export function memoryBucket() { } const metadata = value => Buffer.from(JSON.stringify(value)).toString("base64url"); -const outbound = (path, init, env) => handleMediaOutbound(new Request(`http://media.internal${path}`, init), env); +const outbound = (path, init, env) => handleMediaOutbound(new Request(`http://media.internal${path}`, { ...init, + headers: { ...init?.headers, "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"admin"}' } }), policyFixtureEnv(env)); async function signedLink(secret, id, expires) { return `https://gateway.example/v1/media/files/${id}?expires=${expires}&signature=${ diff --git a/tests/test_media_generation.py b/tests/test_media_generation.py index ebc0c0af..b627f3e1 100644 --- a/tests/test_media_generation.py +++ b/tests/test_media_generation.py @@ -223,11 +223,10 @@ def provider(method, url, **kwargs): stream.status_code = 200 stream.raw = __import__("io").BytesIO(b"mp4-bytes") stream.headers["Content-Type"] = "video/mp4" - with patch("services.video_generation.requests.request", side_effect=provider), \ - patch("services.video_generation.requests.get", return_value=stream) as download: + with patch("services.video_generation.requests.request", side_effect=lambda method, url, **kw: stream if url == "https://vidgen.x.ai/v.mp4" else provider(method, url, **kw)) as download: content = self.client.get(f"/v1/videos/{job['id']}/content", headers=ADMIN) self.assertEqual((content.status_code, content.data, content.mimetype), (200, b"mp4-bytes", "video/mp4")) - self.assertEqual(download.call_args.args[0], "https://vidgen.x.ai/v.mp4") + self.assertEqual(download.call_args.kwargs["url"], "https://vidgen.x.ai/v.mp4") def test_video_jobs_belong_to_their_creator_and_tampered_ids_are_unknown(self): from services import video_generation diff --git a/tests/test_media_jobs_worker.mjs b/tests/test_media_jobs_worker.mjs index 9c1fec11..640ac978 100644 --- a/tests/test_media_jobs_worker.mjs +++ b/tests/test_media_jobs_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { collectContainerEnv } from "../worker/container-env.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; diff --git a/tests/test_media_outbound_worker.mjs b/tests/test_media_outbound_worker.mjs index cb81dea8..111ac6ed 100644 --- a/tests/test_media_outbound_worker.mjs +++ b/tests/test_media_outbound_worker.mjs @@ -1,10 +1,11 @@ +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; -import test from "node:test"; +import test from "./helpers/policy_test.mjs"; import { fetchPublic, handleMediaOutbound, publicHttpsUrl } from "../worker/media-outbound.mjs"; const fetchImage = body => handleMediaOutbound(new Request("http://media.internal/v1/fetch", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }), {}); + method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"fixture-reader"}' }, body: JSON.stringify(body) }), { INTELLIGENCE_DB: policyStore() }); async function withFetch(handler, run) { const original = globalThis.fetch; diff --git a/tests/test_native_token_count.py b/tests/test_native_token_count.py index 7574e1a0..354515ed 100644 --- a/tests/test_native_token_count.py +++ b/tests/test_native_token_count.py @@ -18,7 +18,7 @@ def setUp(self): super().setUp() os.environ["NATIVE_TOKEN_COUNT_ENDPOINTS_JSON"] = json.dumps({"opencode": "/v1/messages/count_tokens"}) self.session = Mock() - self.session.post.return_value = self.upstream({"input_tokens": 123}) + self.session.request.return_value = self.upstream({"input_tokens": 123}) self.session_patch = patch("services.proxy_service.ProxyService._get_provider_session", return_value=self.session) self.session_factory = self.session_patch.start() self.addCleanup(self.session_patch.stop) @@ -109,15 +109,15 @@ def test_native_success_preserves_count_fields_and_strips_model_prefix(self): "tools": [{"name": "f", "input_schema": {"type": "object"}}], "tool_choice": {"type": "auto"}, "thinking": {"type": "enabled", "budget_tokens": 100}, "max_tokens": 999, "stream": True, "temperature": 0.5, "foreign_extra": "omit"} - upstream = self.session.post.return_value + upstream = self.session.request.return_value response = self.post("native", body) self.assertEqual(response.status_code, 200) self.assertEqual(response.json, {"input_tokens": 123}) self.assertEqual(response.headers["X-MultiLLM-Token-Count"], "provider") self.assertNotIn("X-MultiLLM-Token-Count-Fallback", response.headers) self.session_factory.assert_called_once_with("opencode", raw_passthrough=True) - self.session.post.assert_called_once() - kwargs = self.session.post.call_args.kwargs + self.session.request.assert_called_once() + kwargs = self.session.request.call_args.kwargs self.assertEqual(kwargs["url"], "https://opencode.ai/v1/messages/count_tokens") self.assertEqual(kwargs["json"], {key: value for key, value in body.items() if key in {"messages", "system", "tools", "tool_choice", "thinking"}} @@ -143,7 +143,7 @@ def test_header_builder_uses_configured_count_path(self): response = self.post("native", {**self.BODY, "model": "linkapi:claude-model"}) self.assertEqual(response.json, {"input_tokens": 123}) self.assertEqual(prepare.call_args.kwargs["upstream_path"], "/v1/messages/count_tokens") - self.assertEqual(self.session.post.call_args.kwargs["headers"]["X-Api-Key"], "linkapi-provider-key") + self.assertEqual(self.session.request.call_args.kwargs["headers"]["X-Api-Key"], "linkapi-provider-key") def test_unsupported_modes_are_explicit(self): for model in ("auto:route", "minimax-m3", "unknown:model", "opencode:"): @@ -196,10 +196,10 @@ def test_exact_origin_path_replaces_base_prefix(self): for base in ("https://trusted.example/v1", "https://trusted.example/api/coding/v1/", "https://trusted.example:8443/v1"): with self.subTest(base=base), patch.dict(os.environ, {"NATIVE_TOKEN_COUNT_ENDPOINTS_JSON": json.dumps({"opencode": "/api/coding/v1/messages/count_tokens"})}): self.app.config["API_BASE_URLS"] = {"opencode": base} - self.session.post.return_value = self.upstream({"input_tokens": 0}) + self.session.request.return_value = self.upstream({"input_tokens": 0}) self.assertEqual(self.post("native").json, {"input_tokens": 0}) origin = base.split("/v1")[0] if "/api" not in base else "https://trusted.example" - self.assertEqual(self.session.post.call_args.kwargs["url"], origin + "/api/coding/v1/messages/count_tokens") + self.assertEqual(self.session.request.call_args.kwargs["url"], origin + "/api/coding/v1/messages/count_tokens") def test_invalid_trusted_origin_never_dispatches(self): for base in ("file:///v1", "https:///v1", "https://user:password@trusted.example/v1", @@ -215,30 +215,30 @@ def test_upstream_errors_never_fall_back(self): for status in (400, 401, 403, 404, 429, 500, 502, 503, 504): with self.subTest(mode=mode, status=status): upstream = self.upstream({"error": {"message": "provider rejected request"}}, status) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post(mode), status) upstream.close.assert_called_once() - self.assertEqual(self.session.post.call_count, 18) + self.assertEqual(self.session.request.call_count, 18) self.generation.assert_not_called() def test_redirects_are_terminal_protocol_errors(self): upstream = self.upstream(status=307, raw=b"redirect") upstream.headers["Location"] = "https://other.example" - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("auto"), 502) - self.session.post.assert_called_once() + self.session.request.assert_called_once() upstream.close.assert_called_once() def test_timeout_and_transport_failures_do_not_retry(self): for mode in ("auto", "native"): for error, status in ((requests.Timeout("private detail"), 504), (requests.ConnectionError("private detail"), 502)): with self.subTest(mode=mode, error=type(error)): - self.session.post.reset_mock() - self.session.post.side_effect = error + self.session.request.reset_mock() + self.session.request.side_effect = error response = self.post(mode) self.assert_error(response, status) self.assertNotIn("private detail", response.get_data(as_text=True)) - self.session.post.assert_called_once() + self.session.request.assert_called_once() def test_invalid_counts_and_json_are_terminal_errors(self): for body in ({}, [], {"input_tokens": True}, {"input_tokens": "123"}, {"input_tokens": -1}, @@ -246,23 +246,23 @@ def test_invalid_counts_and_json_are_terminal_errors(self): {"input_tokens": float("nan")}, {"input_tokens": 4, "extra": float("nan")}): with self.subTest(body=body): upstream = self.upstream(body) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("auto"), 502) upstream.close.assert_called_once() upstream = self.upstream(raw=b"not json") - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("native"), 502) upstream.close.assert_called_once() def test_response_limit_and_read_timeout_close_once(self): for raw in (b" " * 65537, b'{"input_tokens":1,"padding":"' + b"x" * 65536 + b'"}'): upstream = self.upstream(raw=raw) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("native"), 502) upstream.close.assert_called_once() upstream = self.upstream({"input_tokens": 1}) upstream.iter_content = Mock(side_effect=requests.Timeout("read timed out")) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("auto"), 504) upstream.close.assert_called_once() @@ -271,20 +271,20 @@ def test_wrapped_read_timeout_closes_once(self): upstream = self.upstream({"input_tokens": 1}) upstream.iter_content = Mock(side_effect=requests.ConnectionError(ReadTimeoutError(None, None, "read timeout"))) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assert_error(self.post("auto"), 504) - self.session.post.assert_called_once() + self.session.request.assert_called_once() upstream.close.assert_called_once() def test_exact_response_bound_and_chunked_json(self): raw = b'{"input_tokens":42}' upstream = self.upstream(raw=raw + b" " * (65536 - len(raw))) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assertEqual(self.post("native").json, {"input_tokens": 42}) upstream.close.assert_called_once() upstream = self.upstream() upstream.iter_content = Mock(return_value=iter([b'{"input_', b'tokens":', b"42}"])) - self.session.post.return_value = upstream + self.session.request.return_value = upstream self.assertEqual(self.post("native").json, {"input_tokens": 42}) upstream.close.assert_called_once() @@ -292,7 +292,7 @@ def test_configured_headers_ignore_caller_credentials(self): headers = {**self.HEADERS, "Authorization": "Bearer untrusted-caller-value", "anthropic-beta": "count-test", "X-MultiLLM-Api-Key": "admin-test-key", "x-api-key": "untrusted-caller-value", "Cookie": "stream=true"} self.assertEqual(self.post("native", headers=headers).status_code, 200) - forwarded = self.session.post.call_args.kwargs["headers"] + forwarded = self.session.request.call_args.kwargs["headers"] self.assertEqual(forwarded["Authorization"], "Bearer opencode-provider-key") self.assertEqual(forwarded["Anthropic-Beta"], "count-test") self.assertNotIn("untrusted-caller-value", str(forwarded)) diff --git a/tests/test_opencode_provider.py b/tests/test_opencode_provider.py index e82a3f2c..374f2b22 100644 --- a/tests/test_opencode_provider.py +++ b/tests/test_opencode_provider.py @@ -10,6 +10,12 @@ from flask import Response + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class OpenCodeProviderRouteTest(unittest.TestCase): def setUp(self): self.original_env = os.environ.copy() @@ -493,6 +499,11 @@ def setUp(self): sys.modules.pop(module_name, None) self.proxy_module = importlib.import_module("services.proxy_service") + from services.dispatch_policy import envelope + store = patch("services.auth_service.AuthService._load_user_by_username", return_value={ + "username": "synthetic-transport", "data_policy": envelope({"official_only": None, "forced_provider_preferences": None})}) + store.start() + self.addCleanup(store.stop) def tearDown(self): os.environ.clear() diff --git a/tests/test_opencode_thinking_worker.mjs b/tests/test_opencode_thinking_worker.mjs index 4e376277..f99b96a8 100644 --- a/tests/test_opencode_thinking_worker.mjs +++ b/tests/test_opencode_thinking_worker.mjs @@ -1,6 +1,13 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { previewProfile } from "../worker/roleplay/profile-receipt.mjs"; import { applyReasoningPolicy } from "../worker/roleplay/reasoning.mjs"; import { withOpencodeGlmReasoning } from "../worker/opencode/reasoning-request.mjs"; diff --git a/tests/test_outbound_inventory.py b/tests/test_outbound_inventory.py new file mode 100644 index 00000000..fc05820f --- /dev/null +++ b/tests/test_outbound_inventory.py @@ -0,0 +1,48 @@ +"""Reviewed outbound surfaces: additions or edits require another boundary review. + +This intentionally pins the complete transport module, not just a line containing +fetch: moving admission after a call must also invalidate the inventory. The +manifest records why each transport is guarded or is not a provider dispatch. +""" +import hashlib +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST = ROOT / "tests/outbound_inventory.json" + +def inventory(sources): + return {name: hashlib.sha256(text.encode()).hexdigest() + for name, text in sources.items()} + + +def sources(): + files = [path for suffix in ("*.py", "*.mjs", "*.js", "*.cjs", "*.ts") for path in ROOT.glob(suffix)] + for directory in ("services", "routes", "providers", "utils", "worker"): + for suffix in ("*.py", "*.mjs", "*.js", "*.cjs", "*.ts"): + files.extend((ROOT / directory).rglob(suffix)) + return {str(path.relative_to(ROOT)): path.read_text() for path in files} + + +def test_every_outbound_surface_has_an_unchanged_reviewed_boundary(): + reviewed = json.loads(MANIFEST.read_text()) + assert all(item["reason"] and item["kind"] in {"guarded", "internal", "non-provider"} for item in reviewed.values()) + assert inventory(sources()) == {name: item["sha256"] for name, item in reviewed.items()}, ( + "Outbound transport changed: review every call and update its explicit inventory classification only after checking admission.") + + +def test_inventory_catches_new_modules_and_bypasses_inside_existing_transports(): + original = {"services/example.py": "value = 1\n"} + assert inventory(original) + assert inventory({**original, "services/bypass.py": "import requests as network\nnetwork.post(url, json=body)\n"}) + worker = {"worker/example.mjs": "providerOperation('cloudflare', run)"} + assert inventory(worker) != inventory({"worker/example.mjs": worker["worker/example.mjs"] + "\nreturn fetch(url, {body});"}) + assert inventory({"worker/new.mjs": "return env.AI.fetch(url);"}) + assert inventory({"services/new.py": "from http import client as network\nnetwork.HTTPSConnection(host).request(method, path)"}) + + +def test_inventory_pins_previously_unlisted_modules_and_alternate_clients(): + for module, mutation in [("routes/intelligence.py", "import urllib3\nurllib3.PoolManager().request(method, url)"), + ("worker/new.js", "import axios from 'axios'; axios.get(url)")]: + before = {module: "value = 1"} + assert inventory(before) != inventory({module: before[module] + "\n" + mutation}) diff --git a/tests/test_payment_billing_worker.mjs b/tests/test_payment_billing_worker.mjs index 42ac8a1c..3ea42c65 100644 --- a/tests/test_payment_billing_worker.mjs +++ b/tests/test_payment_billing_worker.mjs @@ -1,10 +1,11 @@ +import { mockFetch } from "./helpers/network_fixture.mjs"; import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; import { DatabaseSync } from "node:sqlite"; import { createHmac } from "node:crypto"; import { fileURLToPath } from "node:url"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { handlePaymentStoreRequest, verifyPaymentSignature, paymentSettings } from "../worker/payment-webhooks.mjs"; @@ -170,7 +171,7 @@ test("Worker forwards unauthenticated webhook raw bytes and signature to the Con const worker = (await loadWorkerModule()).default; const raw = new TextEncoder().encode(' {"fixture":"é"}\r\n'); let forwards = 0; - t.mock.method(globalThis, "fetch", () => assert.fail("unexpected external network")); + mockFetch(t, () => assert.fail("unexpected external network")); const env = { MULTILLM_PROXY_CONTAINER: { getByName(name) { assert.equal(name, "primary"); return { async fetch(request) { diff --git a/tests/test_pii_rehydration_worker.mjs b/tests/test_pii_rehydration_worker.mjs index 6b11ec5e..0388613a 100644 --- a/tests/test_pii_rehydration_worker.mjs +++ b/tests/test_pii_rehydration_worker.mjs @@ -115,7 +115,9 @@ test("event fragments, terminal prefixes, bounds and cancellation", async () => }); test("native roleplay transport redacts after firewall and restores", async () => { - const { attemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { attemptRoleplayCandidates: rawAttemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { policyFixtureEnv } = await import("./helpers/dispatch_fixture.mjs"); + const attemptRoleplayCandidates = (...args) => { args[3] = policyFixtureEnv(args[3]); return rawAttemptRoleplayCandidates(...args); }; const previous = globalThis.fetch; const calls = []; globalThis.fetch = async (_url, init) => { @@ -137,7 +139,9 @@ test("native roleplay transport redacts after firewall and restores", async () = }); test("required failure never dispatches or falls back; best effort preserves payload", async () => { - const { attemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { attemptRoleplayCandidates: rawAttemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { policyFixtureEnv } = await import("./helpers/dispatch_fixture.mjs"); + const attemptRoleplayCandidates = (...args) => { args[3] = policyFixtureEnv(args[3]); return rawAttemptRoleplayCandidates(...args); }; const previous = globalThis.fetch; let calls = 0; globalThis.fetch = async (_url, init) => { calls += 1; return Response.json({ content: JSON.parse(init.body).messages.map(message => message.content).join("") }); }; @@ -168,7 +172,9 @@ test("abort during nonstream restoration clears state and cancels pending reader }); test("restoration failure after output cannot authorize another provider", async () => { - const { attemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { attemptRoleplayCandidates: rawAttemptRoleplayCandidates } = await import("../worker/roleplay/transport.mjs"); + const { policyFixtureEnv } = await import("./helpers/dispatch_fixture.mjs"); + const attemptRoleplayCandidates = (...args) => { args[3] = policyFixtureEnv(args[3]); return rawAttemptRoleplayCandidates(...args); }; const previous = globalThis.fetch; let calls = 0; globalThis.fetch = async () => { calls += 1; return Response.json({ content: "__MLPII_" }); }; diff --git a/tests/test_prompt_cache_cost_worker.mjs b/tests/test_prompt_cache_cost_worker.mjs index 62c84168..a4799faf 100644 --- a/tests/test_prompt_cache_cost_worker.mjs +++ b/tests/test_prompt_cache_cost_worker.mjs @@ -1,14 +1,15 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { readFile } from 'node:fs/promises'; -import { convertV4MiniflareOptions, Miniflare } from 'miniflare'; +import { convertV4MiniflareOptions, Miniflare } from './helpers/local_miniflare.mjs'; import { BUCKET_FIELDS, serializeUsageBuckets, recordUsageWithBuckets, bucketInsertStatement } from '../worker/usage-buckets-d1.mjs'; import { validRow } from '../worker/usage-ledger-d1.mjs'; import { applyMigrations } from './d1_migrations.mjs'; const base = {at:'2026-10-01T00:00:00.000Z', principal:'reader', key_prefix:null, kind:'chat', endpoint:'/v1/chat/completions', requested_model:'openai:m', selected_model:'openai:m', status:200, - latency_ms:1, input_tokens:100, output_tokens:10, cost_usd:0.00019, cost_basis:'usage', request_id:null}; + latency_ms:1, input_tokens:100, output_tokens:10, cost_usd:0.00019, cost_basis:'usage', request_id:null, + cached_input_tokens:null, reasoning_tokens:null}; const buckets = Object.fromEntries(BUCKET_FIELDS.map(name => [name, null])); Object.assign(buckets, {ordinary_input_tokens:40, cache_read_input_tokens:60, cache_write_input_tokens:0, ordinary_input_cost_microusd:80, cache_read_input_cost_microusd:30, cache_write_input_cost_microusd:0, diff --git a/tests/test_prompt_injection_worker.mjs b/tests/test_prompt_injection_worker.mjs index aeafbd0a..efc29256 100644 --- a/tests/test_prompt_injection_worker.mjs +++ b/tests/test_prompt_injection_worker.mjs @@ -1,3 +1,5 @@ +import { mockFetch } from "./helpers/network_fixture.mjs"; +import { policyStore } from "./helpers/dispatch_fixture.mjs"; import assert from "node:assert/strict"; import test from "node:test"; import { readFile } from "node:fs/promises"; @@ -86,7 +88,7 @@ function fixture(mode, extra = {}) { async put(key, value) { if (typeof key === "object") for (const [k, v] of Object.entries(key)) rows.set(k, v); else rows.set(key, structuredClone(value)); }, async delete(key) { rows.delete(key); }, async setAlarm() {} }; - const env = { ADMIN_API_KEY: "synthetic-admin", OPENCODE_GO_API_KEY: "synthetic-provider", + const env = { INTELLIGENCE_DB: policyStore(), ADMIN_API_KEY: "synthetic-admin", OPENCODE_GO_API_KEY: "synthetic-provider", SECRET_SCAN_DEFAULT: "off", PROMPT_INJECTION_MODE: mode, PROMPT_INJECTION_THRESHOLD: "3", ROLEPLAY_COMPACT_TRIGGER_TOKENS: "0", ...extra }; env.ROLEPLAY_SESSION = { getByName(id) { @@ -101,7 +103,7 @@ for (const [mode, status, calls, action] of [["off", 200, 1, null], ["log", 200, test(`authorized roleplay ${mode} dispatch and content-free headers/events`, async t => { const f = fixture(mode), bodies = [], logs = []; t.mock.method(console, "info", (...args) => logs.push(args)); - t.mock.method(globalThis, "fetch", async (_url, init) => { + mockFetch(t, async (_url, init) => { bodies.push(JSON.parse(init.body)); return Response.json({ choices: [{ message: { role: "assistant", content: "Fixture answer." }, finish_reason: "stop" }] }); }); @@ -126,7 +128,7 @@ for (const [mode, status, calls, action] of [["off", 200, 1, null], ["log", 200, test("block precedes compaction and zero-retention storage", async t => { const f = fixture("block", { CONTENT_RETENTION_ENABLED: "true", CONTENT_RETENTION_POLICY_JSON: '{"default":"zero"}' }); let calls = 0; - t.mock.method(globalThis, "fetch", async () => { calls += 1; throw new Error("Unexpected dispatch"); }); + mockFetch(t, async () => { calls += 1; throw new Error("Unexpected dispatch"); }); const response = await handleRoleplayEdgeRequest(new Request("https://proxy.example/v1/roleplay", { method: "POST", headers: { Authorization: "Bearer synthetic-admin", "Content-Type": "application/json" }, body: JSON.stringify({ model: "roleplay:auto", session_id: "zero-injection", stream: false, @@ -176,7 +178,7 @@ for (const [mode, text, bodyPolicy, action] of [ test(`roleplay ${mode} ignores body policy for ${JSON.stringify(text)} and preserves forwarding`, async t => { const f = fixture(mode), bodies = []; t.mock.method(console, "info", () => {}); - t.mock.method(globalThis, "fetch", async (_url, init) => { + mockFetch(t, async (_url, init) => { bodies.push(JSON.parse(init.body)); return Response.json({ choices: [{ message: { role: "assistant", content: "Fixture answer." }, finish_reason: "stop" }] }); }); @@ -208,7 +210,7 @@ test("limits include multibyte text and cyclic structures", () => { test("log header survives upstream failure without changing replay permission", async t => { const f = fixture("log"); let calls = 0; t.mock.method(console, "info", () => {}); - t.mock.method(globalThis, "fetch", async () => { calls += 1; return Response.json({ error: "fixture" }, { status: 400 }); }); + mockFetch(t, async () => { calls += 1; return Response.json({ error: "fixture" }, { status: 400 }); }); const response = await handleRoleplayEdgeRequest(new Request("https://proxy.example/v1/roleplay", { method: "POST", headers: { Authorization: "Bearer synthetic-admin", "Content-Type": "application/json" }, body: JSON.stringify({ model: "roleplay:auto", session_id: "log-failure", stream: false, @@ -220,7 +222,7 @@ test("log header survives upstream failure without changing replay permission", test("unauthorized roleplay does not scan or dispatch", async t => { const f = fixture("block"), logs = []; let calls = 0; t.mock.method(console, "info", (...args) => logs.push(args)); - t.mock.method(globalThis, "fetch", async () => { calls += 1; throw new Error("Unexpected dispatch"); }); + mockFetch(t, async () => { calls += 1; throw new Error("Unexpected dispatch"); }); const response = await handleRoleplayEdgeRequest(new Request("https://proxy.example/v1/roleplay", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload()) }), f.env); assert.equal(response.status, 401); assert.equal(calls, 0); assert.deepEqual(logs, []); @@ -229,7 +231,7 @@ test("unauthorized roleplay does not scan or dispatch", async t => { test("streaming log decision preserves terminal events and one dispatch", async t => { const f = fixture("log"); let calls = 0; t.mock.method(console, "info", () => {}); - t.mock.method(globalThis, "fetch", async () => { + mockFetch(t, async () => { calls += 1; return new Response('data: {"choices":[{"delta":{"content":"Fixture answer."},"finish_reason":null}]}\n\n' + 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', diff --git a/tests/test_prompt_templates_worker.mjs b/tests/test_prompt_templates_worker.mjs index 1e1596cc..027732bb 100644 --- a/tests/test_prompt_templates_worker.mjs +++ b/tests/test_prompt_templates_worker.mjs @@ -4,7 +4,7 @@ import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { applyMigrations, migrationNames } from "./d1_migrations.mjs"; diff --git a/tests/test_provider_assets.py b/tests/test_provider_assets.py new file mode 100644 index 00000000..0d386242 --- /dev/null +++ b/tests/test_provider_assets.py @@ -0,0 +1,128 @@ +"""Assets retain admission without inheriting API credentials or redirect bodies.""" +import io +from unittest.mock import Mock, patch + +import pytest +import requests + +from error_handlers import APIError +from services import media_storage, video_generation +from services.dispatch_policy import envelope +from services.provider_assets import asset_http +from services.provider_dispatch import dispatch_as, provider_http +from tests.unified_api_test_case import UnifiedApiTestCase + + +def response(status=200, location=None): + result = requests.Response() + result.status_code = status + result.raw = io.BytesIO(b"synthetic-video") + result.headers.update({"Content-Type": "video/mp4", "Content-Length": "15"}) + if location is not None: + result.headers["Location"] = location + return result + + +@pytest.fixture +def authority(monkeypatch): + user = {"username": "synthetic-assets", "data_policy": envelope({"official_only": 1, "forced_provider_preferences": None})} + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: user) + monkeypatch.setenv("XAI_API_KEY", "synthetic-operator-secret") + monkeypatch.setenv("OPENAI_API_KEY", "synthetic-operator-secret") + with dispatch_as({"username": user["username"]}): + yield user + + +@pytest.mark.parametrize("consumer", ["relay", "storage"]) +@pytest.mark.parametrize("restricted", [False, True]) +def test_public_video_retrieval_never_receives_credentials(consumer, restricted, authority, monkeypatch): + authority["data_policy"]["official_only"] = 1 if restricted else None + send = Mock(return_value=response()) + monkeypatch.setattr("services.provider_dispatch.requests.request", send) + monkeypatch.setattr(media_storage, "put_stream", lambda *args, **kwargs: {"size": args[2]}) + url = "https://unrelated-cdn.example/asset.mp4" + headers = {"Authorization": "Bearer synthetic-cached", "x-goog-api-key": "synthetic-cached", "Cookie": "synthetic"} + if consumer == "relay": + assert video_generation.stream_content(url, headers, provider="xai").get_data() == b"synthetic-video" + else: + assert media_storage.store_video("mv_synthetic", url, headers, owner=authority["username"], model="xai:synthetic") + assert send.call_args.kwargs["headers"] == {} + + +@pytest.mark.parametrize("consumer", ["relay", "storage"]) +@pytest.mark.parametrize("restricted", [False, True]) +def test_video_redirects_follow_location_and_strip_secrets(consumer, restricted, authority, monkeypatch): + authority["data_policy"]["official_only"] = 1 if restricted else None + send = Mock(side_effect=[response(302, "https://cdn.example/one"), response(307, "/two"), response()]) + monkeypatch.setattr("services.provider_dispatch.requests.request", send) + monkeypatch.setattr(media_storage, "put_stream", lambda *args, **kwargs: {"size": args[2]}) + url = "https://api.openai.com/v1/videos/synthetic/content" + headers = {"Authorization": "Bearer synthetic-cached"} + if consumer == "relay": + assert video_generation.stream_content(url, headers, provider="openai").get_data() == b"synthetic-video" + else: + assert media_storage.store_video("mv_synthetic", url, headers, owner=authority["username"], model="openai:synthetic") + calls = [call.kwargs for call in send.call_args_list] + assert calls[0]["headers"]["Authorization"] == "Bearer " + ("synthetic-operator-secret" if restricted else "synthetic-cached") + assert [call["url"] for call in calls[1:]] == ["https://cdn.example/one", "https://cdn.example/two"] + assert all(call["headers"] == {} and call["allow_redirects"] is False for call in calls[1:]) + + +@pytest.mark.parametrize("consumer", ["relay", "storage"]) +@pytest.mark.parametrize("location", [None, "http://cdn.example/asset", "https://cdn.example/loop"]) +def test_unresolved_or_unsafe_redirects_are_errors(consumer, location, authority, monkeypatch): + send = Mock(side_effect=lambda **kwargs: response(302, location)) + monkeypatch.setattr("services.provider_dispatch.requests.request", send) + put = Mock() + monkeypatch.setattr(media_storage, "put_stream", put) + with pytest.raises(APIError) as error: + if consumer == "relay": + video_generation.stream_content("https://cdn.example/one", {}, provider="xai") + else: + media_storage.store_video("mv_synthetic", "https://cdn.example/one", {}, owner=authority["username"], model="xai:synthetic") + assert error.value.status_code == 502 + assert send.call_count <= 6 + put.assert_not_called() + + +def test_each_redirect_rechecks_policy_and_never_reintroduces_credentials(authority, monkeypatch): + send = Mock(side_effect=[response(302, "https://cdn.example/one"), + response(302, "https://api.openai.com/v1/videos/synthetic/content"), response()]) + monkeypatch.setattr("services.provider_dispatch.requests.request", send) + asset_http("openai", "https://api.openai.com/v1/videos/synthetic/content").close() + assert [c.kwargs["headers"] for c in send.call_args_list][1:] == [{}, {}] + def changed(**kwargs): + authority["data_policy"] = None + return response(302, "https://cdn.example/two") + send.side_effect = changed + with pytest.raises(APIError) as error: + asset_http("xai", "https://cdn.example/one") + assert error.value.status_code == 503 + assert send.call_count == 4 + + +def test_unapproved_api_origin_cannot_receive_attested_secret(authority, monkeypatch): + send = Mock() + with pytest.raises(APIError): + provider_http("xai", "GET", "https://api.x.ai.unrelated.example/v1/videos/synthetic", transport=send) + send.assert_not_called() + + +class RegisteredVideoAssetTest(UnifiedApiTestCase): + def test_restricted_registered_content_route_keeps_status_credential_off_cdn(self): + from tests.test_media_generation import upstream + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("synthetic-assets", is_admin=True) + auth.set_key_controls("synthetic-assets", {"official_only": True}) + with self.app.app_context(): + job = video_generation.issue_job_id("synthetic-assets", "xai", "synthetic", "synthetic-video") + with patch.dict("os.environ", {"XAI_API_KEY": "synthetic-operator-secret"}), \ + patch.object(auth, "get_api_key", return_value="synthetic-cached"), \ + patch("services.provider_dispatch.requests.request", side_effect=[upstream(200, + {"video": {"url": "https://unrelated-cdn.example/asset.mp4"}}), response()]) as send: + result = self.client.get(f"/v1/videos/{job}/content", headers={"Authorization": "Bearer " + created["api_key"]}) + assert result.status_code == 200, result.json + assert result.data == b"synthetic-video" + assert send.call_args_list[0].kwargs["headers"]["Authorization"] == "Bearer synthetic-operator-secret" + assert send.call_args_list[1].kwargs["headers"] == {} diff --git a/tests/test_provider_dispatch.py b/tests/test_provider_dispatch.py new file mode 100644 index 00000000..ab10ff7f --- /dev/null +++ b/tests/test_provider_dispatch.py @@ -0,0 +1,170 @@ +"""All provider adapters share current durable policy and credential admission.""" +import json +import os +from unittest.mock import Mock, patch + +import pytest +from flask import Flask, g + +from error_handlers import APIError +from services.dispatch_policy import envelope, verify +from services.provider_dispatch import capture_dispatch, provider_http +from services.user_store import _row +from tests.unified_api_test_case import UnifiedApiTestCase + + +def account(**controls): + return {"username": "synthetic-user", "api_key_prefix": "synthetic", "data_policy": envelope({ + "official_only": None, "forced_provider_preferences": None, **controls})} + + +@pytest.mark.parametrize("policy", [None, {}, {"version": 0, "official_only": None, "forced_provider_preferences": None}, + {"version": 1, "official_only": None}, {"version": 1, "forced_provider_preferences": None}, + {"version": 1, "official_only": "false", "forced_provider_preferences": None}]) +def test_missing_old_or_malformed_policy_never_authorizes(policy, monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: {**account(), "data_policy": policy}) + send = Mock() + with Flask(__name__).test_request_context(): + g.authenticated_user = {"username": "synthetic-user", "is_admin": True} + with pytest.raises(APIError) as error: + provider_http("openai", "POST", "https://provider.example/v1/chat/completions", transport=send) + assert error.value.status_code == 503 + send.assert_not_called() + + +@pytest.mark.parametrize("provider", ["opencode", "free", "linkapi", "knowledge", "unknown"]) +def test_every_adapter_uses_current_controls_instead_of_cached_principal(provider, monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: account(official_only=1)) + send = Mock() + with Flask(__name__).test_request_context(): + g.authenticated_user = account() + with pytest.raises(APIError) as error: + provider_http(provider, "POST", "https://provider.example/generate", transport=send) + assert error.value.status_code == 403 + send.assert_not_called() + + +def test_deferred_dispatch_captures_identity_but_rereads_controls(monkeypatch): + current = account() + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: current) + send = Mock() + with Flask(__name__).test_request_context(): + g.authenticated_user = current + deferred = capture_dispatch(lambda: provider_http("linkapi", "POST", "https://provider.example/generate", transport=send)) + current = account(official_only=1) + with pytest.raises(APIError) as error: + deferred() + assert error.value.status_code == 403 + send.assert_not_called() + + +def test_forced_preferences_merge_at_actual_http_boundary(monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: account(forced_provider_preferences={"data_collection": "deny"})) + monkeypatch.setenv("OPENROUTER_API_KEY", "synthetic-operator") + send = Mock() + with Flask(__name__).test_request_context(): + g.authenticated_user = account() + provider_http("openrouter", "POST", "https://openrouter.ai/api/v1/chat/completions", transport=send, + json={"provider": {"data_collection": "allow", "sort": "price"}}, headers={"Authorization": "Bearer synthetic-cached"}) + assert send.call_args.kwargs["json"]["provider"] == {"data_collection": "deny", "sort": "price"} + assert send.call_args.kwargs["headers"]["Authorization"] == "Bearer synthetic-operator" + assert send.call_args.kwargs["allow_redirects"] is False + + +@pytest.mark.parametrize("policy", [None, {}, {"version": 2, "official_only": None, "forced_provider_preferences": None}]) +def test_mixed_version_rpc_rows_fail_closed(policy): + from services.user_store import USER_FIELDS + row = dict.fromkeys(USER_FIELDS) + row.update(username="synthetic-user", api_key_hash="synthetic-hash", api_key_prefix="synthetic", scopes="chat", is_admin=0, + created_at="2026-10-20T00:00:00Z") + if policy is not None: + row["data_policy"] = policy + with pytest.raises(APIError): + _row(row) + row["data_policy"] = envelope(row) + assert _row(row)["official_only"] is None + assert verify(row["data_policy"])["version"] == 1 + + +class ProviderDispatchRouteTest(UnifiedApiTestCase): + def setUp(self): + super().setUp() + self.app.config["GEMINI_DATA_TIER"] = "paid" + os.environ["GEMINI_API_KEY"] = "AIza-attested-synthetic" + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("synthetic-video", is_admin=True) + auth.set_key_controls("synthetic-video", {"official_only": True}) + self.headers = {"Authorization": "Bearer " + created["api_key"]} + + def test_gemini_video_creation_reselects_attested_credential(self): + from tests.test_media_generation import upstream + with patch.object(self.app_module.AuthService, "get_api_key", return_value="AIza-stale-synthetic"), \ + patch("services.provider_dispatch.requests.request", return_value=upstream(200, {"name": "operations/synthetic"})) as send: + response = self.client.post("/v1/videos", headers=self.headers, + json={"model": "gemini:veo-3.1-generate-preview", "prompt": "Synthetic landscape"}) + assert response.status_code == 200, response.json + assert send.call_args.kwargs["headers"]["x-goog-api-key"] == "AIza-attested-synthetic" + + def test_video_overrides_and_missing_attestation_do_not_dispatch(self): + for query, extra, missing in [("?key=caller-synthetic", {}, False), ("", {"x-goog-api-key": "caller-synthetic"}, False), ("", {}, True)]: + if missing: + os.environ.pop("GEMINI_API_KEY") + with patch.object(self.app_module.AuthService, "get_api_key", return_value="AIza-stale-synthetic"), \ + patch("services.provider_dispatch.requests.request") as send: + response = self.client.post("/v1/videos" + query, headers={**self.headers, **extra}, + json={"model": "gemini:veo-3.1-generate-preview", "prompt": "Synthetic landscape"}) + assert response.status_code == (503 if missing else 403), response.json + send.assert_not_called() + + def test_bootstrap_recovery_does_not_grant_provider_authority(self): + from services.auth_service import AuthService + AuthService._forget_verified_keys() + with patch.object(AuthService, "_load_user_by_username", side_effect=APIError("Synthetic store unavailable", 503)), \ + patch("requests.sessions.Session.request") as send: + response = self.client.post("/v1/chat/completions", headers={"Authorization": "Bearer admin-test-key"}, + json={"model": "openai:synthetic", "messages": [{"role": "user", "content": "Synthetic"}]}) + assert response.status_code == 503, response.json + send.assert_not_called() + + +@pytest.mark.parametrize("response", [{"version": 1, "principal": None}, {"version": 0, "principal": account()}, + {"version": 1, "principal": {"username": "integration:synthetic"}}, + {"version": 1, "principal": {"username": "integration:synthetic", "data_policy": {"version": 1, "official_only": None}}}]) +def test_integration_dispatch_rejects_old_or_incomplete_worker_authority(response, monkeypatch): + monkeypatch.setattr("services.intelligence_d1_store.request_private_intelligence", lambda *args, **kwargs: response) + send = Mock() + with pytest.raises(APIError) as error: + provider_http("openai", "POST", "https://provider.example/generate", reference={"username": "integration:synthetic"}, transport=send) + assert error.value.status_code == 503 + send.assert_not_called() + + +@pytest.mark.parametrize("operation", ["poll", "content", "watch"]) +def test_video_deferred_operations_recheck_current_policy_and_attestation(operation, monkeypatch): + from services import video_generation + from services.provider_dispatch import dispatch_as + app = Flask(__name__) + app.config["GEMINI_DATA_TIER"] = "paid" + current = account(official_only=1) + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: current) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + send = Mock() + job = {"p": "gemini", "u": "operations/synthetic", "m": "veo-synthetic", "t": 1} + def execute(): + if operation == "content": + return video_generation.stream_content("https://generativelanguage.googleapis.com/v1beta/files/synthetic.mp4", {"x-goog-api-key": "synthetic-cached"}, provider="gemini") + return video_generation.job_status(job, "synthetic-job", "synthetic-cached", "https://provider.example") + with app.test_request_context(): + g.authenticated_user = current + deferred = capture_dispatch(execute) + with patch("services.provider_dispatch.requests.request", send), dispatch_as({"username": "synthetic-user"}): + with pytest.raises(APIError) as error: + deferred() + assert error.value.status_code == 503 + monkeypatch.setenv("GEMINI_API_KEY", "synthetic-current") + app.config["GEMINI_DATA_TIER"] = "free" + with pytest.raises(APIError) as error: + deferred() + assert error.value.status_code == 403 + send.assert_not_called() diff --git a/tests/test_provider_dispatch_worker.mjs b/tests/test_provider_dispatch_worker.mjs new file mode 100644 index 00000000..be9437f4 --- /dev/null +++ b/tests/test_provider_dispatch_worker.mjs @@ -0,0 +1,151 @@ +import assert from "node:assert/strict"; +import { providerBinding } from "./helpers/provider_binding.mjs"; +import test from "node:test"; +import { providerFetch, providerOperation, withDispatchPrincipal } from "../worker/provider-dispatch.mjs"; +import { verifyDataPolicy } from "../worker/dispatch-policy.mjs"; +import { handleAiOutbound } from "../worker/ai-outbound.mjs"; +import { policyStore } from "./helpers/dispatch_fixture.mjs"; +import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { makeRoleplayEnv, roleplayRequest, withGlobalFetch } from "./helpers/roleplay_fixture.mjs"; + +const worker = (await loadWorkerModule()).default; +const authority = { authenticated: true, keyId: "synthetic-reader" }; +const input = (provider = "openrouter") => new Request((provider === "gemini" ? "https://generativelanguage.googleapis.com" : "https://openrouter.ai") + "/v1/chat/completions", { + method: "POST", headers: { "content-type": "application/json", authorization: "Bearer synthetic-cached" }, + body: JSON.stringify({ provider: { data_collection: "allow", sort: "price" }, messages: [] }), +}); +const controls = () => ({ official_only: null, forced_provider_preferences: null }); + +for (const row of [null, {}, { official_only: null }, { forced_provider_preferences: null }, + { official_only: 1, forced_provider_preferences: "[]" }]) { + test(`an incomplete or malformed durable policy refuses HTTP and SDK dispatch: ${JSON.stringify(row)}`, async () => { + const env = { INTELLIGENCE_DB: policyStore(row) }; + await assert.rejects(providerFetch("openai", input(), {}, { env, authority, send: () => assert.fail("HTTP dispatch") }), + { code: "key_data_controls_unavailable", status: 503 }); + await assert.rejects(withDispatchPrincipal(env, authority, () => providerOperation("cloudflare", () => assert.fail("SDK dispatch"))), + { code: "key_data_controls_unavailable", status: 503 }); + }); +} + +test("a versioned envelope requires both explicit fields and the supported version", () => { + for (const value of [null, {}, { version: 0, ...controls() }, { version: true, ...controls() }, + { version: 1, official_only: null }, { version: 1, ...controls(), extra: null }]) { + assert.throws(() => verifyDataPolicy(value)); + } + assert.deepEqual(verifyDataPolicy({ version: 1, ...controls() }), { version: 1, ...controls() }); +}); + +test("each retry, poll and fallback rereads current controls rather than a captured policy", async () => { + const row = controls(), env = { INTELLIGENCE_DB: policyStore(row) }; + let calls = 0; + const send = () => { calls++; return new Response("{}"); }; + await providerFetch("linkapi", input(), {}, { env, authority, send }); + row.official_only = 1; + for (const provider of ["linkapi", "nanogpt", "opencode", "free", "unknown"]) { + await assert.rejects(providerFetch(provider, input(), {}, { env, authority, send }), { code: "provider_class_not_allowed", status: 403 }); + } + assert.equal(calls, 1); +}); + +test("forced routing preferences and current attested credentials replace cached headers at dispatch", async () => { + const env = { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: '{"data_collection":"deny"}' }), + OPENROUTER_API_KEY: "synthetic-current" }; + await providerFetch("openrouter", input(), {}, { env, authority, send: async request => { + assert.equal(request.headers.get("authorization"), "Bearer synthetic-current"); + assert.deepEqual((await request.json()).provider, { data_collection: "deny", sort: "price" }); + assert.equal(request.redirect, "manual"); + return new Response("{}"); + } }); + await assert.rejects(providerFetch("openrouter", input(), {}, { env, authority, realtime: true }), + { code: "forced_provider_preferences_not_supported", status: 403 }); + delete env.OPENROUTER_API_KEY; + await assert.rejects(providerFetch("openrouter", input(), {}, { env, authority }), { code: "attested_credential_unavailable", status: 503 }); +}); + +test("Gemini never uses cached or query credentials for a restricted principal", async () => { + const env = { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }), GEMINI_DATA_TIER: "paid" }; + await assert.rejects(providerFetch("gemini", input("gemini"), {}, { env, authority }), { code: "attested_credential_unavailable" }); + env.GEMINI_API_KEY = "synthetic-attested"; + await assert.rejects(providerFetch("gemini", "https://provider.example/generate?key=synthetic-caller", {}, { env, authority }), + { code: "provider_credential_not_allowed", status: 403 }); + await providerFetch("gemini", input("gemini"), {}, { env, authority, send: request => { + assert.equal(request.headers.get("authorization"), null); + assert.equal(request.headers.get("x-goog-api-key"), "synthetic-attested"); + return new Response("{}"); + } }); +}); + +test("private inference bindings refuse missing identity and unavailable current authority before SDK calls", async () => { + let calls = 0; + const env = { AI: providerBinding({ run() { calls++; assert.fail("SDK dispatch"); } }) }; + for (const headers of [{}, { "X-MultiLLM-Dispatch-Principal": '{"version":0,"username":"synthetic-reader"}' }, + { "X-MultiLLM-Dispatch-Principal": '{"version":1,"username":"synthetic-reader"}' }]) { + const result = await handleAiOutbound(new Request("http://ai.internal/v1/embeddings", { method: "POST", headers: { "content-type": "application/json", ...headers }, + body: JSON.stringify({ model: "@cf/baai/bge-m3", input: "Synthetic" }) }), env); + assert.equal(result.status, 503); + } + assert.equal(calls, 0); +}); + +for (const [name, row, expected] of [["official", { official_only: 1, forced_provider_preferences: null }, 403], + ["forced", { official_only: null, forced_provider_preferences: '{"data_collection":"deny"}' }, 403], + ["old schema", {}, 503], ["missing account", null, 503]]) { + test(`registered roleplay refuses ${name} controls before generation, compaction or fallback`, async () => { + const fixture = makeRoleplayEnv({ INTELLIGENCE_DB: policyStore(row) }); + let calls = 0; + const response = await withGlobalFetch(() => { calls++; assert.fail("Roleplay upstream dispatch"); }, () => worker.fetch( + roleplayRequest({ messages: [{ role: "user", content: "Synthetic turn" }], stream: false, model: "roleplay:glm" }), + fixture.env, { waitUntil() {} })); + assert.equal(response.status, expected, await response.clone().text()); + assert.equal(calls, 0); + }); +} + +test("registered roleplay refuses an unreadable control store even with an administrator credential", async () => { + const fixture = makeRoleplayEnv({ INTELLIGENCE_DB: { prepare() { throw Error("Synthetic outage"); } } }); + await withGlobalFetch(() => assert.fail("Roleplay upstream dispatch"), async () => { + const response = await worker.fetch(roleplayRequest({ messages: [{ role: "user", content: "Synthetic turn" }], + model: "roleplay:glm", stream: false }), fixture.env, { waitUntil() {} }); + assert.equal(response.status, 503, await response.clone().text()); + }); +}); + + +test("index polling rereads current controls before every SDK call", async () => { + const { KnowledgeCorpus } = await import("../worker/knowledge/corpus.mjs"); + let controls = { official_only: null, forced_provider_preferences: null }, calls = 0; + const env = { INTELLIGENCE_DB: { prepare(query) { + return { bind() { return this; }, async first() { return controls; } }; + } }, KNOWLEDGE_INDEX: providerBinding({ search() { assert.fail("Unexpected search"); }, items: { get() { return { async info() { + calls++; return { id: "synthetic-item", key: "revisions/synthetic.txt", status: "completed" }; + } }; } } }) }; + const corpus = new KnowledgeCorpus(env); + const artifact = { snapshot_key: "snapshots/synthetic.txt", index_key: "revisions/synthetic.txt", + content_hash: "a".repeat(64), byte_length: 1 }; + await withDispatchPrincipal(env, { authenticated: true, keyId: "synthetic" }, async () => { + assert.equal((await corpus.reconcileRevision(artifact, "synthetic-item")).status, "completed"); + controls = { official_only: 1 }; + await assert.rejects(() => corpus.reconcileRevision(artifact, "synthetic-item"), { code: "key_data_controls_unavailable" }); + controls = null; + await assert.rejects(() => corpus.reconcileRevision(artifact, "synthetic-item"), { code: "key_data_controls_unavailable" }); + }); + assert.equal(calls, 1); +}); + + +test("import watches use the source account instead of the scheduler and refuse legacy ownerless imports", async () => { + const { checkImports } = await import("../worker/knowledge/skills-import.mjs"); + const env = { INTELLIGENCE_DB: policyStore({ official_only: 1, forced_provider_preferences: null }) }; + const source = { source: "github", repository: "example/synthetic", path: "", ref: "HEAD", commit: "a".repeat(40) }; + for (const dispatch_principal of [undefined, { authenticated: true, keyId: "source-key" }]) { + let checked = 0, calls = 0; + const store = { async call(name, body) { + if (name === "imports.due") return [{ skill_id: "synthetic", origin: { ...source, dispatch_principal } }]; + assert.equal(name, "imports.checked"); assert.equal(body.result.status, "error"); checked++; + } }; + await withDispatchPrincipal({ INTELLIGENCE_DB: policyStore() }, { authenticated: true, keyId: "admin" }, async () => { + assert.equal(await checkImports(env, store, { fetch() { calls++; assert.fail("Watch dispatch"); } }), 1); + }); + assert.equal(calls, 0); assert.equal(checked, 1); + } +}); diff --git a/tests/test_rate_limit_headers.py b/tests/test_rate_limit_headers.py index dca580b7..7e1ec945 100644 --- a/tests/test_rate_limit_headers.py +++ b/tests/test_rate_limit_headers.py @@ -194,7 +194,8 @@ def test_usage_registered_route_default_and_bounded_snapshot(monkeypatch): monkeypatch.setattr(usage, "usage_history", lambda *args, **kwargs: {}) monkeypatch.setattr(usage.BudgetService, "status", lambda _: {}) monkeypatch.setattr(usage.key_controls, "public", lambda _: { - "allowed_models": None, "allowed_ips": None, "expires_at": None}) + "allowed_models": None, "allowed_ips": None, "expires_at": None, + "official_only": None, "forced_provider_preferences": None}) @app.before_request def authenticate(): g.authenticated_user = USER diff --git a/tests/test_raw_passthrough.py b/tests/test_raw_passthrough.py index be5448f9..24a3b36e 100644 --- a/tests/test_raw_passthrough.py +++ b/tests/test_raw_passthrough.py @@ -11,6 +11,12 @@ from werkzeug.datastructures import MultiDict + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class RawPassthroughTest(unittest.TestCase): def setUp(self): self.original_env = os.environ.copy() diff --git a/tests/test_realtime_worker.mjs b/tests/test_realtime_worker.mjs index 0776c2fa..8e6f1454 100644 --- a/tests/test_realtime_worker.mjs +++ b/tests/test_realtime_worker.mjs @@ -46,6 +46,7 @@ async function fixture(t) { return { success: true, results: [], meta: { changes: Number(stmt.run(...values).changes) } }; } }; }, async batch(statements) { sql.exec("BEGIN"); try { const results = statements.map(stmt => stmt.execute()); sql.exec("COMMIT"); return results; } catch (error) { sql.exec("ROLLBACK"); throw error; } } }; + sql.exec("INSERT INTO control_users (username, api_key_hash, api_key_prefix, scopes, is_admin, created_at) VALUES ('admin', 'synthetic-hash', 'synthetic', 'admin,chat,audio', 1, '2026-10-20T00:00:00Z')"); const server = new Socket(), client = new Socket(), upstream = new Socket(), timers = clock(), pending = [], leases = []; const env = { ...enabled, INTELLIGENCE_DB: db, ADMISSION_COORDINATOR: { getByName() { return { async fetch(req) { const value = await req.json(); leases.push(value); @@ -83,8 +84,9 @@ test("invalid mappings disable once without logging values", () => { } } finally { console.warn = old; } assert.equal(messages.length, 1); assert.ok(!messages.join().includes("password")); }); -test("registered enabled errors never wake Container", async () => { - const env = { ...enabled, MULTILLM_PROXY_CONTAINER: { getByName() { throw Error("Container woke"); } } }; +test("registered enabled errors never wake Container", async t => { + const f = await fixture(t); + const env = { ...f.env, MULTILLM_PROXY_CONTAINER: { getByName() { throw Error("Container woke"); } } }; for (const [req, expected] of [[request({ upgrade: "" }), 426], [request({ authorization: "" }), 401], [new Request("https://gateway.example/v1/realtime?model=openai:unmapped", { headers: { upgrade: "websocket", authorization: "Bearer synthetic-gateway" } }), 403]]) { const response = await worker.fetch(req, env, {}); assert.equal(response.status, expected); assert.ok((await response.json()).error.code); @@ -188,7 +190,7 @@ test("dashboard key model grants, scopes, expiry and tickets use current D1 auth assert.equal((await handleRealtimeRequest(request({ authorization: `Bearer ${key}` }), f.env, f.ctx, f.options)).status, 403); f.sql.prepare("UPDATE control_users SET scopes='chat,audio',expires_at='2000-01-01T00:00:00Z' WHERE username='fixture-user'").run(); assert.equal((await handleRealtimeRequest(request({ authorization: `Bearer ${key}` }), f.env, f.ctx, f.options)).status, 401); - f.sql.prepare("UPDATE control_users SET expires_at=NULL WHERE username='fixture-user'").run(); + f.sql.prepare("UPDATE control_users SET expires_at=NULL,official_only=1 WHERE username='fixture-user'").run(); assert.equal((await handleRealtimeRequest(request({ authorization: `Bearer ${key}` }), f.env, f.ctx, f.options)).status, 101); f.server.emit("close", { code: 1000 }); await f.flush(); assert.equal(f.calls(), 1); }); @@ -258,3 +260,69 @@ test("missing lease, storage failure and budget exhaustion never dial", async t assert.ok(!JSON.stringify(await response.json()).includes("private storage details")); } }); + +async function controlledAccount(t, selected = "linkapi:realtime-test") { + const f = await fixture(t), key = "synthetic-controlled-key", salt = "syntheticsalt"; + const hash = `scrypt:32768:8:1$${salt}$${scryptSync(key, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }).toString("hex")}`; + f.env.AUTH_STORAGE_BACKEND = "d1"; + f.env.REALTIME_PROVIDERS_JSON = JSON.stringify({ [selected]: { url: "wss://approved.example/v1/realtime?model=realtime-test" } }); + f.env.REALTIME_PRICING_JSON = JSON.stringify({ [selected]: prices }); + f.env.LINKAPI_KEY = f.env.OPENROUTER_API_KEY = "synthetic-provider"; + f.sql.prepare(`INSERT INTO control_users(username,api_key_hash,api_key_prefix,scopes,is_admin,created_at) + VALUES (?,?,?,?,0,?)`).run("controlled", hash, `mllm_${key.slice(0, 8)}`, "chat,audio", new Date().toISOString()); + const socket = (token = key) => new Request(`https://gateway.example/v1/realtime?model=${selected}`, { + headers: { upgrade: "websocket", authorization: `Bearer ${token}` } }); + const ticket = () => new Request("https://gateway.example/v1/realtime/client_secrets", { + method: "POST", headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, + body: JSON.stringify({ model: selected }) }); + return { ...f, socket, ticket }; +} + +test("registered Realtime sockets and ticket issuance enforce provider classes", async t => { + const f = await controlledAccount(t); + f.sql.prepare("UPDATE control_users SET official_only=1 WHERE username='controlled'").run(); + const original = globalThis.fetch; let dials = 0; + globalThis.fetch = async () => { dials++; assert.fail("Restricted Realtime must not dial upstream"); }; + try { + for (const request of [f.socket(), f.ticket()]) { + const response = await worker.fetch(request, f.env, f.ctx); + assert.equal(response.status, 403); + assert.equal((await response.json()).error.code, "provider_class_not_allowed"); + } + assert.equal(dials, 0); + assert.equal(f.sql.prepare("SELECT COUNT(*) AS n FROM realtime_tickets").get().n, 0); + } finally { globalThis.fetch = original; } +}); + +for (const selected of ["openrouter:realtime-test", "openai:realtime-test"]) { + test(`registered Realtime refuses forced preferences for ${selected.split(":")[0]}`, async t => { + const f = await controlledAccount(t, selected); + f.sql.prepare("UPDATE control_users SET forced_provider_preferences=? WHERE username='controlled'") + .run('{"data_collection":"deny"}'); + for (const request of [f.socket(), f.ticket()]) { + const response = await worker.fetch(request, f.env, f.ctx); + assert.equal(response.status, 403); + assert.equal((await response.json()).error.code, "forced_provider_preferences_not_supported"); + } + assert.equal(f.calls(), 0); + }); +} + +test("ticket consumption rechecks changed data controls before consumption or dispatch", async t => { + const f = await controlledAccount(t); + const issued = await worker.fetch(f.ticket(), f.env, f.ctx); + assert.equal(issued.status, 200); + const { value } = await issued.json(); + for (const [official, preferences, code] of [[1, null, "provider_class_not_allowed"], + [0, '{"data_collection":"deny"}', "forced_provider_preferences_not_supported"], + [0, "invalid", "realtime_auth_unavailable"]]) { + f.sql.prepare("UPDATE control_users SET official_only=?,forced_provider_preferences=? WHERE username='controlled'") + .run(official, preferences); + const response = await worker.fetch(f.socket(value), f.env, f.ctx); + assert.equal(response.status, preferences === "invalid" ? 503 : 403); + assert.equal((await response.json()).error.code, code); + assert.equal(f.sql.prepare("SELECT consumed_at FROM realtime_tickets").get().consumed_at, null); + assert.equal(f.sql.prepare("SELECT COUNT(*) AS n FROM realtime_sessions").get().n, 0); + assert.equal(f.calls(), 0); + } +}); diff --git a/tests/test_request_cancellation_worker.mjs b/tests/test_request_cancellation_worker.mjs index b110ba35..318c0ae8 100644 --- a/tests/test_request_cancellation_worker.mjs +++ b/tests/test_request_cancellation_worker.mjs @@ -1,8 +1,10 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const attemptRoleplayCandidates = (...args) => { args[3] = policyFixtureEnv(args[3]); return rawAttemptRoleplayCandidates(...args); }; import assert from "node:assert/strict"; import test from "node:test"; import { UpstreamCancellation } from "../worker/upstream-cancellation.mjs"; -import { attemptRoleplayCandidates, readBoundedBytes } from "../worker/roleplay/transport.mjs"; +import { attemptRoleplayCandidates as rawAttemptRoleplayCandidates, readBoundedBytes } from "../worker/roleplay/transport.mjs"; async function withGlobalFetch(fetchImpl, callback) { const previous = globalThis.fetch; diff --git a/tests/test_retry_advice.py b/tests/test_retry_advice.py index bb233204..bf23b241 100644 --- a/tests/test_retry_advice.py +++ b/tests/test_retry_advice.py @@ -8,6 +8,11 @@ import requests + +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + @pytest.fixture(autouse=True) def settings(monkeypatch): monkeypatch.delenv("UPSTREAM_RETRY_AFTER_ADVICE_ENABLED", raising=False) diff --git a/tests/test_route_health_worker.mjs b/tests/test_route_health_worker.mjs index af1e81ca..c392d76f 100644 --- a/tests/test_route_health_worker.mjs +++ b/tests/test_route_health_worker.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { fetchIfRunning, runScheduledHealth, scheduleSettings } from "../worker/health-schedule.mjs"; diff --git a/tests/test_scim_provisioning_worker.mjs b/tests/test_scim_provisioning_worker.mjs index 7073e786..28ee8367 100644 --- a/tests/test_scim_provisioning_worker.mjs +++ b/tests/test_scim_provisioning_worker.mjs @@ -25,7 +25,7 @@ function account(n = 1, changes = {}) { function database(t, migrated = true) { const sql = new DatabaseSync(":memory:"); t.after(() => sql.close()); - for (const file of ["0003_control_users.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0017_control_revisions.sql"]) + for (const file of ["0003_control_users.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0017_control_revisions.sql", "0041_key_data_guards.sql"]) sql.exec(schema(file)); sql.exec("CREATE TABLE retained_usage (username TEXT, amount INTEGER); INSERT INTO retained_usage VALUES ('user-1', 7)"); sql.exec("CREATE TABLE fake_teams (org TEXT, id TEXT, name TEXT, active INTEGER, PRIMARY KEY(org,id))"); diff --git a/tests/test_secret_firewall.py b/tests/test_secret_firewall.py index 813eda07..370b1aa3 100644 --- a/tests/test_secret_firewall.py +++ b/tests/test_secret_firewall.py @@ -11,6 +11,11 @@ TOKEN = "AK" + "IA" + "AB12CD34EF56GH78" + +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + @pytest.fixture(autouse=True) def reset_audit_window(monkeypatch): monkeypatch.setattr(firewall, "_audit_events", OrderedDict()) diff --git a/tests/test_secret_firewall_coverage_worker.mjs b/tests/test_secret_firewall_coverage_worker.mjs index 2a0e9144..8ab8da33 100644 --- a/tests/test_secret_firewall_coverage_worker.mjs +++ b/tests/test_secret_firewall_coverage_worker.mjs @@ -14,7 +14,8 @@ test("new edge dispatches require a firewall coverage review", () => { assert.ok(!/await fetch\(upstreamRequest/.test(source)); const roleplay = readFileSync(new URL("../worker/roleplay/transport.mjs", import.meta.url), "utf8"); assert.ok(roleplay.includes("await firewallFetch(new Request")); - assert.ok(roleplay.includes("await containerNamespace.getByName")); + assert.ok(roleplay.includes("transformed => providerFetch(candidate.provider")); + assert.ok(roleplay.includes('send: async permitted => useOpenCodeContainer ? containerNamespace.getByName("primary").fetch(permitted)')); }); test("new edge routes and modules require reviewing the dispatch inventory", () => { diff --git a/tests/test_secret_firewall_worker.mjs b/tests/test_secret_firewall_worker.mjs index fa225ed1..0612d806 100644 --- a/tests/test_secret_firewall_worker.mjs +++ b/tests/test_secret_firewall_worker.mjs @@ -8,7 +8,7 @@ const worker = (await loadWorkerModule()).default; for (const mode of ["off", "observe", "redact", "block"]) test(`direct edge provider ${mode}`, async () => { const events = [], bodies = []; const env = { ADMIN_API_KEY: "synthetic-bootstrap", OPENCODE_GO_API_KEY: "synthetic-upstream", OPENCODE_EDGE_FETCH: "true", - SECRET_SCAN_DEFAULT: mode, INTELLIGENCE_DB: { prepare() { return { bind(...args) { events.push(args); return { run: async () => ({}) }; } }; } } }; + SECRET_SCAN_DEFAULT: mode, INTELLIGENCE_DB: { prepare(sql) { if (sql.startsWith("SELECT official_only")) return { bind() { return { first: async () => ({ official_only: null, forced_provider_preferences: null }) }; } }; return { bind(...args) { events.push(args); return { run: async () => ({}) }; } }; } } }; const original = globalThis.fetch; globalThis.fetch = async req => { bodies.push(await req.text()); return Response.json({ choices: [{ message: { content: "ok" } }] }); }; let response; diff --git a/tests/test_shadow_eval.py b/tests/test_shadow_eval.py index c6b93fdd..ebdff04d 100644 --- a/tests/test_shadow_eval.py +++ b/tests/test_shadow_eval.py @@ -30,6 +30,8 @@ def store(tmp_path, monkeypatch): monkeypatch.setenv("CONTROL_PLANE_DATABASE_URL", "") monkeypatch.setenv("MODEL_REGISTRY_DB_PATH", str(tmp_path / "eval.sqlite3")) monkeypatch.delenv("SHADOW_EVAL_JUDGE_MODEL", raising=False) + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", + lambda username: {"username": username, "data_policy": {"version": 1, "official_only": None, "forced_provider_preferences": None}} if username == "sampled-user" else None) return Store @@ -94,8 +96,8 @@ def test_key_rate_validation(rate): def test_key_rate_storage_and_default(): assert key_controls.validate({})["shadow_eval_rate"] is None assert key_controls.validate({"shadow_eval_rate": "0.1"})["shadow_eval_rate"] == 0.1 - assert key_controls.public(key_controls.from_storage({"shadow_eval_rate": 0.1}))["shadow_eval_rate"] == 0.1 - assert key_controls.from_storage({"shadow_eval_rate": 1})["shadow_eval_rate"] is None + assert key_controls.public(key_controls.from_storage({"shadow_eval_rate": 0.1, "official_only": None, "forced_provider_preferences": None}))["shadow_eval_rate"] == 0.1 + assert key_controls.from_storage({"shadow_eval_rate": 1, "official_only": None, "forced_provider_preferences": None})["shadow_eval_rate"] is None def test_store_ttl_cap_and_on_demand_text(store): diff --git a/tests/test_shadow_eval_data_guards.py b/tests/test_shadow_eval_data_guards.py new file mode 100644 index 00000000..60632256 --- /dev/null +++ b/tests/test_shadow_eval_data_guards.py @@ -0,0 +1,89 @@ +"""Controlled source content cannot reach replay candidates or judges.""" + +import time +from unittest.mock import Mock + +import pytest +from flask import Flask, Response, g + +from services.dispatch_policy import envelope +from services.shadow_eval_contract import default_config, encoded, make_sample +from services.shadow_eval_data_policy import source_allows_replay +from services.shadow_eval_runner import evaluate, run +from services.shadow_eval_sampling import sample_success +from tests.test_shadow_eval import configured, response, sample, store # noqa: F401 + +CONTROLS = [{"official_only": True}, {"forced_provider_preferences": {"data_collection": "deny"}}, + {"forced_provider_preferences": {}}] + + +@pytest.mark.parametrize("controls", CONTROLS) +@pytest.mark.parametrize("stream", [False, True]) +def test_controlled_requests_never_enter_sampling(controls, stream, monkeypatch): + submit = Mock() + draw = Mock(return_value=0) + monkeypatch.setattr("services.shadow_eval_sampling.submit", submit) + monkeypatch.setattr("services.shadow_eval_sampling.random.random", draw) + payload = {"model": "auto:example", "messages": [{"role": "user", "content": "Synthetic"}]} + app = Flask(__name__) + with app.test_request_context("/v1/chat/completions", method="POST", json=payload): + g.authenticated_user = {"username": "sampled-user", "shadow_eval_rate": 0.2, **controls} + original = Response(iter([b"data: [DONE]\n\n"]), mimetype="text/event-stream") if stream else response("Synthetic") + assert sample_success(original, payload) is original + if stream: + assert list(original.response) == [b"data: [DONE]\n\n"] + submit.assert_not_called() + draw.assert_not_called() + assert make_sample(payload, controls, "auto:example", {"content": "Synthetic"}, + "openai:production", 1, {}) is None + + +@pytest.mark.parametrize("controls", CONTROLS) +def test_legacy_samples_from_controlled_keys_skip_candidate_and_judges(store, controls, monkeypatch): + configured(store, max_replays_per_run=1) + store.put(sample()) + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: {"username": "sampled-user", **controls}) + dispatch = Mock() + app = Flask(__name__) + with app.test_request_context(): + assert run(app, dispatch, store=store) == 0 + dispatch.assert_not_called() + assert store.results() == [] + + +@pytest.mark.parametrize("controls", CONTROLS) +@pytest.mark.parametrize("change_after", [0, 1, 2]) +def test_source_policy_rechecked_before_candidate_and_each_judge(controls, change_after, monkeypatch): + current = {"username": "sampled-user", "official_only": None, "forced_provider_preferences": None, **(controls if change_after == 0 else {})} + current["data_policy"] = envelope(current) + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: current.copy()) + monkeypatch.setattr("services.shadow_eval_runner.accounted_dispatch", lambda payload, dispatch, **_: dispatch(payload)) + calls = [] + + def dispatch(payload): + calls.append(payload["model"]) + if len(calls) == change_after: + current.update(controls) + current["data_policy"] = envelope(current) + if len(calls) > 1: + return response(encoded({"winner": "A", "confidence": 0.9, "reasons": ["Synthetic"]})) + return response("Candidate synthetic") + + with Flask(__name__).test_request_context(): + result = evaluate(sample(), "openrouter:example/model", default_config(), dispatch, deadline=time.monotonic() + 10) + assert len(calls) == change_after + assert calls == ["openrouter:example/model", "free:json"][:change_after] + assert result["outcome"] == "failed" + + +@pytest.mark.parametrize("source", [None, {"revoked_at": "2026-10-20"}, {"expires_at": "2000-01-01T00:00:00Z"}]) +def test_unknown_or_inactive_source_authority_refuses_replay(source, monkeypatch): + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: source) + assert not source_allows_replay(sample()) + + +def test_unavailable_source_authority_refuses_replay(monkeypatch): + def unavailable(_): + raise RuntimeError("Synthetic storage failure") + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", unavailable) + assert not source_allows_replay(sample()) diff --git a/tests/test_shadow_eval_worker.mjs b/tests/test_shadow_eval_worker.mjs index c58cf004..0a2d7133 100644 --- a/tests/test_shadow_eval_worker.mjs +++ b/tests/test_shadow_eval_worker.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; -import { convertV4MiniflareOptions, Miniflare } from 'miniflare'; +import { convertV4MiniflareOptions, Miniflare } from './helpers/local_miniflare.mjs'; import { applyMigrations } from './d1_migrations.mjs'; import { handleShadowEvalRequest, validConfig, validSample, validResult } from '../worker/shadow-eval-d1.mjs'; import { runScheduledShadowEval } from '../worker/shadow-eval-schedule.mjs'; diff --git a/tests/test_shared_generation_cache_worker.mjs b/tests/test_shared_generation_cache_worker.mjs index 574ee176..79771860 100644 --- a/tests/test_shared_generation_cache_worker.mjs +++ b/tests/test_shared_generation_cache_worker.mjs @@ -1,3 +1,12 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, keyId: authority?.principal?.id ?? "admin", ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; @@ -5,14 +14,16 @@ import { DatabaseSync } from "node:sqlite"; import { GenerationCacheD1, cleanupGenerationCache, CACHE_PREFIX } from "../worker/generation-cache-d1.mjs"; import { generationCacheSettings, exactCacheIdentity, eligibleRequest, prepareNativeCache } from "../worker/exact-generation-cache.mjs"; import { completeCacheBody } from "../worker/generation-cache-d1.mjs"; -import { nativeGenerationFetch } from "../worker/gateway-extensions.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch } from "../worker/gateway-extensions.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { handleControlStateRequest } from "../worker/control-state-d1.mjs"; const MIGRATION = "0019_generation_cache.sql"; function database() { const db = new DatabaseSync(":memory:"); db.exec(readFileSync(new URL(`../intelligence-migrations/${MIGRATION}`, import.meta.url), "utf8")); + for (const file of ["0003_control_users.sql", "0041_key_data_guards.sql"]) + db.exec(readFileSync(new URL(`../intelligence-migrations/${file}`, import.meta.url), "utf8")); const execute = (sql, args) => { const numbered = /\?[1-9]/.test(sql); const statement = db.prepare(sql.replace(/\?(\d+)/g, (_, number) => `$v${number}`)); diff --git a/tests/test_skills_clients.py b/tests/test_skills_clients.py index 3c9e2a9e..9c3acc04 100644 --- a/tests/test_skills_clients.py +++ b/tests/test_skills_clients.py @@ -1,5 +1,6 @@ """Local-only client tests with temporary libraries and a synthetic HTTP server.""" +from contextvars import copy_context import importlib.util import json from pathlib import Path @@ -11,6 +12,8 @@ import pytest +from services.dispatch_network import control_network + ROOT = Path(__file__).resolve().parents[1] @@ -153,7 +156,8 @@ def log_message(self, *_args): server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() - yield f"http://127.0.0.1:{server.server_port}", received + with control_network(f"http://127.0.0.1:{server.server_port}"): + yield f"http://127.0.0.1:{server.server_port}", received server.shutdown() server.server_close() thread.join() @@ -235,7 +239,9 @@ def test_hook_real_rest_find_uses_fast_limit_three(fake_server, monkeypatch): base_url, received = fake_server monkeypatch.setenv("MULTILLM_BASE_URL", base_url) monkeypatch.setenv("MULTILLM_KNOWLEDGE_API_KEY", "synthetic-hook-key") - result = hook.hint({"prompt": "Test regression behavior"}) + context = copy_context() + result = hook.hint({"prompt": "Test regression behavior"}, + fetcher=lambda *args, **kwargs: context.run(hook.fetch, *args, **kwargs)) assert "Relevant skills: Testing" in result["hookSpecificOutput"]["additionalContext"] assert received == [{"query": "Test regression behavior", "mode": "fast", "limit": 3, "min_confidence": "high"}] diff --git a/tests/test_stream_cost_breaker.py b/tests/test_stream_cost_breaker.py index b5f565f0..f562501a 100644 --- a/tests/test_stream_cost_breaker.py +++ b/tests/test_stream_cost_breaker.py @@ -184,6 +184,8 @@ def test_strict_d1_rows_missing_schema_and_off_wire(monkeypatch): monkeypatch.setattr(user_store,"_call",lambda *a,**k:{"user":row}) with pytest.raises(APIError) as error: breaker.get_user("reader") assert error.value.status_code==503 and error.value.payload["error"]=="stream_cost_storage_unavailable" + from services.dispatch_policy import envelope + row["data_policy"] = envelope(row) row["max_stream_cost_microusd"]=4 assert breaker.get_user("reader")["max_stream_cost_microusd"]==4 row["unknown"]=None diff --git a/tests/test_stream_cost_breaker_worker.mjs b/tests/test_stream_cost_breaker_worker.mjs index 1f83f953..c1bfbb76 100644 --- a/tests/test_stream_cost_breaker_worker.mjs +++ b/tests/test_stream_cost_breaker_worker.mjs @@ -67,7 +67,7 @@ function fakeDb({missing=false,stored={}}={}) { } async function call(db,body,flags=env) { return handleControlUsersRequest(new Request("http://intelligence.internal/v1/users",{method:"POST", - headers:{"content-type":"application/json"},body:JSON.stringify({version:1,...body})}),{...flags,INTELLIGENCE_DB:db}); + headers:{"content-type":"application/json"},body:JSON.stringify({version:1,data_policy_version:1,...body})}),{...flags,INTELLIGENCE_DB:db}); } test("strict caps and omitted updates preserve existing column",async()=>{ for(const value of [-1,0.5,true,NaN,1e16]) assert.equal(validUser(account({max_stream_cost_microusd:value})),false); diff --git a/tests/test_tenant_hierarchy_worker.mjs b/tests/test_tenant_hierarchy_worker.mjs index a3456599..4fbfd84b 100644 --- a/tests/test_tenant_hierarchy_worker.mjs +++ b/tests/test_tenant_hierarchy_worker.mjs @@ -1,3 +1,6 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, keyId: authority?.principal?.id ?? "admin", ...authority }, ...rest); import assert from "node:assert/strict"; import test from "node:test"; import { DatabaseSync } from "node:sqlite"; @@ -8,7 +11,7 @@ import { handleRealtimeRequest } from "../worker/realtime.mjs"; import { activeUsersByPrefix, USER_FIELDS } from "../worker/control-users-d1.mjs"; import { handleTenantRequest, organisationsEnabled, resolveAccountTenant, tenantNamespace } from "../worker/tenants-d1.mjs"; import { handleManagedStateRequest } from "../worker/managed-state-dispatch.mjs"; -import { nativeGenerationFetch } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch } from "../worker/gateway-extensions.mjs"; test("disabled tenant domain and resolver touch no storage", async () => { const db = { prepare() { throw Error("No storage"); } }; @@ -189,12 +192,12 @@ test("native requests ignore headers and body scope and resolve before dispatch" }); test("prefix lookup leaves tenant resolution to the verified principal", async () => { - const row = { username: "alice" }; + const row = { username: "alice", official_only: null, forced_provider_preferences: null }; const db = { prepare(sql) { assert.ok(sql.includes("api_key_prefix=?")); return { bind() { return this; }, async all() { return { results: [row] }; } }; } }; - assert.deepEqual(await activeUsersByPrefix(db, "fixture", { ORGANISATIONS_ENABLED: "true" }), [row]); + assert.deepEqual(await activeUsersByPrefix(db, "fixture", { ORGANISATIONS_ENABLED: "true" }), [{ ...row, data_policy: { version: 1, official_only: null, forced_provider_preferences: null } }]); assert.equal(Object.hasOwn(row, "tenant_context"), false); }); @@ -223,10 +226,10 @@ function authFixture() { async first() { return sql.includes("intelligence_credentials") ? integrationRow : null; }, async run() { calls.writes++; return { meta: { changes: 1 } }; } }; } }; - const env = { ...f.env, INTELLIGENCE_DB: db, AUTH_STORAGE_BACKEND: "d1", ADMIN_API_KEY: "synthetic-bootstrap", + const env = policyFixtureEnv({ ...f.env, INTELLIGENCE_DB: db, AUTH_STORAGE_BACKEND: "d1", ADMIN_API_KEY: "synthetic-bootstrap", ADMIN_USERNAME: "operator", JWT_SECRET: "synthetic-signing", REALTIME_ENABLED: "true", REALTIME_PROVIDERS_JSON: '{"openai:realtime-test":{"url":"wss://approved.example/realtime"}}', - KNOWLEDGE_SERVICE: { async fetch() { calls.dispatches++; return Response.json({ version: 1, result: { status: "ok" } }); } } }; + KNOWLEDGE_SERVICE: { async fetch() { calls.dispatches++; return Response.json({ version: 1, result: { status: "ok" } }); } } }); return { ...f, env, calls }; } const knowledgeRequest = (key, mcp = false) => new Request(mcp ? "https://gateway.example/mcp" : "https://gateway.example/v1/knowledge/artifacts/synthetic", diff --git a/tests/test_transport_resilience.py b/tests/test_transport_resilience.py index 9bd8cfc0..b624c176 100644 --- a/tests/test_transport_resilience.py +++ b/tests/test_transport_resilience.py @@ -8,6 +8,12 @@ import requests + +import pytest +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + class FakeSession: def __init__(self, responses): self.responses = list(responses) diff --git a/tests/test_upstream_outcome.py b/tests/test_upstream_outcome.py index 4d6703e2..01efb963 100644 --- a/tests/test_upstream_outcome.py +++ b/tests/test_upstream_outcome.py @@ -31,6 +31,11 @@ SUCCESS_STATUSES = [200, 201, 204, 206, 299] + +from tests.dispatch_fixture import verified_transport_principal # noqa: F401 + +pytestmark = pytest.mark.usefixtures("verified_transport_principal") + @pytest.fixture def check(): return TestCase() diff --git a/tests/test_usage_ledger.py b/tests/test_usage_ledger.py index 14573a3e..a2854783 100644 --- a/tests/test_usage_ledger.py +++ b/tests/test_usage_ledger.py @@ -178,7 +178,7 @@ def fake(payload, *, endpoint): if payload["operation"] == "totals": return {"version": 1, "totals": {"day_usd": 1.5, "month_usd": 3.0, "day_requests": 2, "month_requests": 4}} if payload["operation"] == "summary": - return {"version": 1, "rows": [{"model": "openai:gpt-4.1", **dict.fromkeys(usage_store.TOTAL_COLUMNS[:7], 1), + return {"version": 1, "rows": [{"model": "openai:gpt-4.1", **dict.fromkeys(usage_store.SUMMARY_COLUMNS, 1), "latency_buckets": [1] + [0] * 10}]} raise PrivateIntelligenceError(503, "storage_unavailable") diff --git a/tests/test_usage_ledger_worker.mjs b/tests/test_usage_ledger_worker.mjs index 9b79675a..dd38cba0 100644 --- a/tests/test_usage_ledger_worker.mjs +++ b/tests/test_usage_ledger_worker.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { handleUsageLedgerRequest, latencyBucket, LATENCY_BUCKETS, validRow } from "../worker/usage-ledger-d1.mjs"; @@ -64,8 +64,8 @@ test("a flush stores raw rows and daily totals once, even when the batch is retr assert.equal(recent[0].status, 502); const older = (await call({ operation: "recent", since: "2026-09-01T00:00:00.000Z", principal: null, before: recent[0].id, limit: 10 })).body.rows; assert.deepEqual(older.map(item => [item.principal, item.status]), [["alice", 200]]); - assert.deepEqual(Object.keys(recent[0]).sort(), ["at", "cost_basis", "cost_usd", "endpoint", "id", "input_tokens", "key_prefix", "kind", - "latency_ms", "output_tokens", "principal", "request_id", "requested_model", "selected_model", "status"]); + assert.deepEqual(Object.keys(recent[0]).sort(), ["at", "cached_input_tokens", "cost_basis", "cost_usd", "endpoint", "id", "input_tokens", "key_prefix", "kind", + "latency_ms", "output_tokens", "principal", "reasoning_tokens", "request_id", "requested_model", "selected_model", "status"]); const pruned = (await call({ operation: "prune", events_before: "2026-09-26T00:00:00.000Z", rollups_before: "2026-09-26", limit: 100 })).body; assert.deepEqual(pruned.pruned, { events: 1, batches: 0, rollups: 1 }); diff --git a/tests/test_usage_receipts_worker.mjs b/tests/test_usage_receipts_worker.mjs index 847caa99..60ac4249 100644 --- a/tests/test_usage_receipts_worker.mjs +++ b/tests/test_usage_receipts_worker.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { readFileSync } from "node:fs"; import { DatabaseSync } from "node:sqlite"; -import { convertV4MiniflareOptions, Miniflare } from "miniflare"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; import { canonicalBytes, appendReceipt, verifyChain, handleUsageReceiptRequest, handleUsageReceiptStoreRequest } from "../worker/usage-receipts.mjs"; import { handleManagedStateRequest } from "../worker/managed-state-dispatch.mjs"; import { handleUsageLedgerRequest } from "../worker/usage-ledger-d1.mjs"; @@ -145,7 +145,7 @@ test("private dispatcher flag, target, missing table and size bounds precede sto test("ledger flush records once, default rows and responses stay unchanged, missing key preserves usage", async t => { const { env, sql } = database(t); sql.exec("CREATE TABLE control_users (id TEXT PRIMARY KEY)"); - for (const migration of ["0007_usage_ledger.sql", "0016_usage_buckets.sql"]) { + for (const migration of ["0007_usage_ledger.sql", "0016_usage_buckets.sql", "0042_usage_token_details.sql"]) { sql.exec(readFileSync(new URL(`../intelligence-migrations/${migration}`, import.meta.url), "utf8")); } const row = { at: "2026-10-09T00:00:00Z", principal: "alice", key_prefix: null, kind: "chat", endpoint: "/v1/chat/completions", diff --git a/tests/test_usage_reservations_worker.mjs b/tests/test_usage_reservations_worker.mjs index 6e4e117f..7210e4af 100644 --- a/tests/test_usage_reservations_worker.mjs +++ b/tests/test_usage_reservations_worker.mjs @@ -214,7 +214,11 @@ test("registered native dispatch preserves response bytes and completes the inje const { operation, id: ignored, ...values } = reserve(); return values; } }); - const { nativeGenerationFetch } = await import("../worker/gateway-extensions.mjs"); + const { nativeGenerationFetch: rawNativeGenerationFetch } = await import("../worker/gateway-extensions.mjs"); + const { policyFixtureEnv } = await import("./helpers/dispatch_fixture.mjs"); + const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, keyId: authority?.principal?.id ?? "admin", ...authority }, ...rest); + const body = '{"choices":[],"usage":{"prompt_tokens":10,"completion_tokens":20}}'; const response = await nativeGenerationFetch(new Request("http://localhost/openai/v1/chat/completions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "test" }), diff --git a/tests/test_usage_token_details.py b/tests/test_usage_token_details.py new file mode 100644 index 00000000..474da3c2 --- /dev/null +++ b/tests/test_usage_token_details.py @@ -0,0 +1,32 @@ +"""Provider-reported token details remain separate from optional price buckets.""" + +import json + +from services import usage_store +from services.usage_types import StreamUsageObserver, UsageObservation + + +def test_usage_details_survive_partial_streams_and_sql_rollups(tmp_path, monkeypatch): + monkeypatch.setenv("USAGE_DB_PATH", str(tmp_path / "usage.sqlite3")) + monkeypatch.setenv("CONTROL_PLANE_DATABASE_URL", "") + observer = StreamUsageObserver(65536) + for body in ({"usage": {"prompt_tokens": 100, "prompt_tokens_details": {"cached_tokens": 60}}}, + {"usage": {"completion_tokens": 20, "completion_tokens_details": {"reasoning_tokens": 8}}}): + observer.feed(("data: " + json.dumps(body) + "\n\n").encode()) + observation = observer.finish() + assert (observation.input_tokens, observation.cached_input_tokens, observation.reasoning_tokens) == (100, 60, 8) + from tests.test_usage_ledger import row + store = usage_store.SqlUsageStore() + store.record("a" * 32, [{**row(tokens=(100, 20)), "cached_input_tokens": 60, "reasoning_tokens": 8}]) + for group in ("model", "principal"): + [summary] = store.summary(group, "2000-01-01", "2100-01-01", None, 10) + assert (summary["cached_input_tokens"], summary["reasoning_tokens"]) == (60, 8) + assert usage_store.summarize(summary)["cache_hit_rate"] == 60 + native = UsageObservation.from_body({"usage": {"input_tokens": 10, "output_tokens": 3, + "cache_read_input_tokens": 60, "cache_creation_input_tokens": 30}}) + assert (native.input_tokens, native.cached_input_tokens) == (100, 60) + responses = UsageObservation.from_body({"response": {"usage": {"input_tokens": 100, + "input_tokens_details": {"cached_tokens": 60}, "output_tokens_details": {"reasoning_tokens": 8}}}}) + assert (responses.cached_input_tokens, responses.reasoning_tokens) == (60, 8) + assert UsageObservation.from_body({"usage": {"prompt_tokens_details": {"cached_tokens": -1}, + "completion_tokens_details": {"reasoning_tokens": True}}}).cached_input_tokens is None diff --git a/tests/test_usage_token_details_worker.mjs b/tests/test_usage_token_details_worker.mjs new file mode 100644 index 00000000..83b86655 --- /dev/null +++ b/tests/test_usage_token_details_worker.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { convertV4MiniflareOptions, Miniflare } from "./helpers/local_miniflare.mjs"; +import { createUsageObserver } from "../worker/request-telemetry.mjs"; +import { handleUsageLedgerRequest, recordNativeUsage } from "../worker/usage-ledger-d1.mjs"; +import { applyMigrations } from "./d1_migrations.mjs"; + +async function database(t) { + const mf = new Miniflare(convertV4MiniflareOptions({ modules: true, + script: "export default {fetch(){return new Response('ok')}}", d1Databases: ["INTELLIGENCE_DB"] })); + t.after(() => mf.dispose()); + const db = await mf.getD1Database("INTELLIGENCE_DB"); + await applyMigrations(db); + return { INTELLIGENCE_DB: db }; +} + +test("reported cache and reasoning tokens persist in atomic D1 rollups", async t => { + const env = await database(t); + const observer = createUsageObserver(true); + for (const usage of [{ prompt_tokens: 100, prompt_tokens_details: { cached_tokens: 60 } }, + { completion_tokens: 20, completion_tokens_details: { reasoning_tokens: 8 } }]) { + observer.feed(new TextEncoder().encode(`data: ${JSON.stringify({ usage })}\n\n`)); + } + const { usage } = observer.finish(); + assert.deepEqual(usage, { input_tokens: 100, output_tokens: 20, cached_input_tokens: 60, reasoning_tokens: 8 }); + await recordNativeUsage(env, { ...usage, provider: "openrouter", model: "example/model", principal: "example", + status: 200, endpoint: "/v1/chat/completions", duration_ms: 5, requestId: "a".repeat(32), cost_usd: null, cost_basis: null }); + const stored = await env.INTELLIGENCE_DB.prepare("SELECT cached_input_tokens, reasoning_tokens FROM usage_events").first(); + assert.deepEqual(stored, { cached_input_tokens: 60, reasoning_tokens: 8 }); + for (const group of ["model", "principal"]) { + const response = await handleUsageLedgerRequest(new Request("http://intelligence.internal/v1/usage", { + method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ version: 1, operation: "summary", + group, since: "2000-01-01", until: "2100-01-01", principal: null, limit: 10 }), + }), env); + const [row] = (await response.json()).rows; + assert.equal(row.cached_input_tokens, 60); + assert.equal(row.reasoning_tokens, 8); + } +}); diff --git a/tests/test_user_store_d1.py b/tests/test_user_store_d1.py index 9805ae28..9be78516 100644 --- a/tests/test_user_store_d1.py +++ b/tests/test_user_store_d1.py @@ -8,6 +8,7 @@ from error_handlers import APIError from services import user_store +from services.dispatch_policy import envelope from services.auth_service import AuthService from services.intelligence_d1_store import PrivateIntelligenceError @@ -35,7 +36,7 @@ def __call__(self, payload, *, endpoint): if row["api_key_prefix"] == payload["prefix"] and row["revoked_at"] is None]} if operation == "upsert": assert set(payload["user"]) == set(user_store.USER_FIELDS) - self.rows[payload["user"]["username"]] = dict(payload["user"]) + self.rows[payload["user"]["username"]] = {**payload["user"], "data_policy": envelope(payload["user"])} return {"version": 1, "stored": True} if operation == "delete": return {"version": 1, "deleted": self.rows.pop(payload["username"], None) is not None} diff --git a/tests/test_vertex_dispatch.py b/tests/test_vertex_dispatch.py new file mode 100644 index 00000000..e22d723a --- /dev/null +++ b/tests/test_vertex_dispatch.py @@ -0,0 +1,124 @@ +"""Restricted Vertex calls refresh only the current operator credential binding.""" +import json +from types import SimpleNamespace +from unittest.mock import Mock, patch + +import pytest +from flask import Flask, g + +from error_handlers import APIError +from services.dispatch_network import network_admitted +from services.dispatch_policy import envelope +from services.provider_dispatch import provider_http +from tests.unified_api_test_case import UnifiedApiTestCase + +URL = "https://us-central1-aiplatform.googleapis.com/v1/projects/synthetic/locations/us-central1/endpoints/openapi/chat/completions" + + +def binding(name="synthetic-a"): + return json.dumps({"type": "service_account", "client_email": name + "@synthetic.iam.gserviceaccount.com", + "token_uri": "https://oauth2.googleapis.com/token"}) + + +@pytest.fixture +def vertex(monkeypatch): + app = Flask(__name__) + app.config["API_BASE_URLS"] = {"googleai": URL} + monkeypatch.setattr("services.auth_service.AuthService._load_user_by_username", lambda _: {"username": "synthetic", "data_policy": envelope({ + "official_only": 1, "forced_provider_preferences": None})}) + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", raising=False) + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) + monkeypatch.setenv("GOOGLEAI_API_KEY", "synthetic-unattested-key") + with app.test_request_context(): + g.authenticated_user = {"username": "synthetic"} + yield + + +def credentials(token="synthetic-current"): + result = SimpleNamespace(token=token) + def refresh(request): + assert network_admitted() + result.refresh = Mock(side_effect=refresh) + return result + + +@pytest.mark.parametrize("source", ["json", "file"]) +def test_current_service_account_binding_overrides_cached_token(vertex, monkeypatch, tmp_path, source): + path = tmp_path / "operator.json" + def set_binding(value): + if source == "json": + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", value) + else: + path.write_text(value) + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", str(path)) + send = Mock() + current = credentials("synthetic-first") + set_binding(binding()) + with patch("google.oauth2.service_account.Credentials.from_service_account_info", return_value=current) as build: + provider_http("googleai", "POST", URL, transport=send, headers={"Authorization": "Bearer synthetic-cached"}) + assert send.call_args.kwargs["headers"]["Authorization"] == "Bearer synthetic-first" + current.token = "synthetic-second" + set_binding(binding("synthetic-b")) + provider_http("googleai", "POST", URL, transport=send, headers={"Authorization": "Bearer synthetic-first"}) + assert send.call_args.kwargs["headers"]["Authorization"] == "Bearer synthetic-second" + assert build.call_args.args[0]["client_email"].startswith("synthetic-b@") + assert current.refresh.call_count == 2 + + +@pytest.mark.parametrize("mode", ["missing", "malformed", "missing-file", "changed-during-refresh", "untrusted-token-origin"]) +def test_unverified_or_changed_service_account_cannot_dispatch(vertex, monkeypatch, mode): + send = Mock() + monkeypatch.setattr("services.provider_credentials.shutil.which", lambda _: None) + if mode == "malformed": + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", "{}") + elif mode == "missing-file": + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", "/nonexistent/synthetic.json") + elif mode == "untrusted-token-origin": + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", binding().replace("oauth2.googleapis.com", "unrelated.example")) + elif mode == "changed-during-refresh": + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", binding()) + current = credentials() + if mode == "changed-during-refresh": + current.refresh.side_effect = lambda _: monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS_JSON", binding("synthetic-b")) + with patch("google.oauth2.service_account.Credentials.from_service_account_info", return_value=current): + with pytest.raises(APIError) as error: + provider_http("googleai", "POST", URL, transport=send, headers={"Authorization": "Bearer synthetic-cached"}) + assert error.value.status_code == 503 + send.assert_not_called() + + +@pytest.mark.parametrize("changed", [False, True]) +def test_gcloud_binds_a_fresh_token_to_the_current_operator_account(vertex, monkeypatch, changed): + monkeypatch.setattr("services.provider_credentials.shutil.which", lambda _: "/trusted/gcloud") + answers = ["synthetic-a", "synthetic-current", "synthetic-b" if changed else "synthetic-a"] + run = Mock(side_effect=lambda *args, **kwargs: SimpleNamespace(stdout=answers.pop(0))) + monkeypatch.setattr("services.provider_credentials.subprocess.run", run) + send = Mock() + if changed: + with pytest.raises(APIError): + provider_http("googleai", "POST", URL, transport=send) + send.assert_not_called() + else: + provider_http("googleai", "POST", URL, transport=send) + assert send.call_args.kwargs["headers"]["Authorization"] == "Bearer synthetic-current" + assert run.call_args_list[1].args[0] == ["/trusted/gcloud", "auth", "print-access-token", "synthetic-a", "--quiet"] + + +class RegisteredVertexDispatchTest(UnifiedApiTestCase): + def test_restricted_native_vertex_route_uses_current_service_account(self): + from tests.test_media_generation import upstream + auth = self.app_module.AuthService + with patch.object(auth, "get_current_user", return_value={"username": "admin", "is_admin": True}): + created = auth.create_user("synthetic-vertex", is_admin=True) + auth.set_key_controls("synthetic-vertex", {"official_only": True}) + self.app.config["API_BASE_URLS"]["googleai"] = URL.rsplit("/", 1)[0] + with patch.dict("os.environ", {"GOOGLE_APPLICATION_CREDENTIALS_JSON": binding(), "PROJECT_ID": "synthetic", + "LOCATION": "us-central1", "GOOGLE_ENDPOINT": "us-central1-aiplatform.googleapis.com"}), \ + patch("google.oauth2.service_account.Credentials.from_service_account_info", return_value=credentials()), \ + patch.object(auth, "get_google_token", return_value="synthetic-cached"), \ + patch("requests.sessions.Session.request", return_value=upstream(200, + {"choices": [{"message": {"role": "assistant", "content": "Synthetic"}}]})) as send: + result = self.client.post("/googleai/v1/chat/completions", headers={"Authorization": "Bearer " + created["api_key"]}, + json={"model": "synthetic", "messages": [{"role": "user", "content": "Synthetic"}]}) + assert result.status_code == 200, result.json + assert send.call_args.kwargs["headers"]["Authorization"] == "Bearer synthetic-current" diff --git a/tests/test_wave3_hub_worker.mjs b/tests/test_wave3_hub_worker.mjs index e6e27a84..92a572da 100644 --- a/tests/test_wave3_hub_worker.mjs +++ b/tests/test_wave3_hub_worker.mjs @@ -1,7 +1,16 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, keyId: authority?.principal?.id ?? "admin", ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; -import { nativeGenerationFetch } from "../worker/gateway-extensions.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch } from "../worker/gateway-extensions.mjs"; import { principalHash, admissionModelGroup } from "../worker/admission-do.mjs"; import { nativeRevisionConsumer } from "../worker/native-config-sync.mjs"; @@ -45,10 +54,10 @@ function fixture(overrides = {}) { : { lease: { lease_id: "a".repeat(32), expires_at: Math.min(Date.now() + 29000, body.deadline_ms) } }) }); } }; } }; env.INTELLIGENCE_DB = { prepare(sql) { return { sql, values: [], bind(...values) { this.values = values; return this; }, - async all() { return { results: [] }; } }; }, async batch(statements) { + async all() { return { results: [] }; }, async first() { return null; } }; }, async batch(statements) { rows.push(...JSON.parse(statements[1].values[0])); return [{ meta: { changes: 1 } }]; } }; - return { env, operations, rows, ctx: { waitUntil(promise) { pending.push(promise); } }, + return { env: policyFixtureEnv(env), operations, rows, ctx: { waitUntil(promise) { pending.push(promise); } }, async flush() { await Promise.all(pending); } }; } async function upstream(fetcher, run) { @@ -141,7 +150,8 @@ test("native revision installers are strict and the injectable clock bounds fres let now = 100, offline = false; const f = fixture({ CONFIG_REVISION_SYNC_ENABLED: "true" }); const db = f.env.INTELLIGENCE_DB; - db.prepare = sql => ({ sql, bind() { return this; }, async all() { + db.prepare = sql => ({ sql, bind() { return this; }, async first() { if (offline) throw Error("offline"); return sql.startsWith("SELECT official_only") + ? { official_only: null, forced_provider_preferences: null } : null; }, async all() { if (offline) throw new Error("offline"); return { results: [] }; } }); diff --git a/tests/test_wave3_transport_worker.mjs b/tests/test_wave3_transport_worker.mjs index 4e79051f..10b7a170 100644 --- a/tests/test_wave3_transport_worker.mjs +++ b/tests/test_wave3_transport_worker.mjs @@ -16,7 +16,7 @@ const clock = Date.parse("2026-10-09T00:00:00Z"); const enabled = { CONTENT_RETENTION_ENABLED: "true" }; const base = { at: new Date(clock).toISOString(), principal: "reader", key_prefix: null, kind: "chat", endpoint: "/v1/chat/completions", requested_model: "openai:fixture", selected_model: "openai:fixture", - status: 200, latency_ms: 300, input_tokens: 100, output_tokens: 10, cost_usd: 0.001, cost_basis: "usage", request_id: null }; + status: 200, latency_ms: 300, input_tokens: 100, output_tokens: 10, cost_usd: 0.001, cost_basis: "usage", request_id: null, cached_input_tokens: null, reasoning_tokens: null }; const buckets = { ordinary_input_tokens: 40, cache_read_input_tokens: 60, cache_write_input_tokens: 0, ordinary_input_cost_microusd: 80, cache_read_input_cost_microusd: 30, cache_write_input_cost_microusd: 0, output_cost_microusd: 80, bucket_basis: "measured", bucket_source: "openai" }; @@ -33,6 +33,7 @@ async function ledger(t, migrated = true) { t.after(() => sqlite.close()); const migration = await readFile(new URL("../intelligence-migrations/0007_usage_ledger.sql", import.meta.url), "utf8"); sqlite.exec(migration.slice(migration.indexOf("CREATE TABLE IF NOT EXISTS usage_events"))); + sqlite.exec(await readFile(new URL("../intelligence-migrations/0042_usage_token_details.sql", import.meta.url), "utf8")); const extend = async () => sqlite.exec(await readFile(new URL("../intelligence-migrations/0016_usage_buckets.sql", import.meta.url), "utf8")); if (migrated) await extend(); const statements = []; diff --git a/tests/test_wave4_worker.mjs b/tests/test_wave4_worker.mjs index 1bf1a23f..4b23c8b1 100644 --- a/tests/test_wave4_worker.mjs +++ b/tests/test_wave4_worker.mjs @@ -1,14 +1,23 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import test from "node:test"; import assert from "node:assert/strict"; import { DatabaseSync } from "node:sqlite"; import { readFileSync } from "node:fs"; -import { nativeGenerationFetch, forwardedGenerationHeaders, nativeGenerationSetup, generationErrorResponse } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch, forwardedGenerationHeaders, nativeGenerationSetup, generationErrorResponse } from "../worker/gateway-extensions.mjs"; import { runScheduledMaintenance, scheduledMaintenanceEnabled, collectAlertAggregates } from "../worker/scheduled-maintenance.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { handleAlertState, normalizeAlertRules } from "../worker/alert-delivery.mjs"; import { retentionRequestId } from "../worker/retention-policy.mjs"; import { generationDeadlineHook } from "../worker/generation-deadline.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; const authority = { route: "/openai/v1/chat/completions", provider: "openai", principal: { id: "admin" } }; const completed = JSON.stringify({ choices: [{ message: { content: "fixture" }, finish_reason: "stop" }], usage: { prompt_tokens: 3, completion_tokens: 2 } }); diff --git a/tests/test_wave5_worker.mjs b/tests/test_wave5_worker.mjs index 4df1e6c4..b6a9a651 100644 --- a/tests/test_wave5_worker.mjs +++ b/tests/test_wave5_worker.mjs @@ -1,15 +1,26 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; import { DatabaseSync } from "node:sqlite"; import { readFileSync } from "node:fs"; -import { nativeGenerationFetch, nativeCacheHeader } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch, nativeCacheHeader } from "../worker/gateway-extensions.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { runScheduledMaintenance, scheduledMaintenanceEnabled } from "../worker/scheduled-maintenance.mjs"; import { ObservabilityExporter, setObservabilityExporter } from "../worker/observability-export.mjs"; import { guardHash } from "../worker/semantic-generation-cache.mjs"; import { prepareCanary } from "../worker/canary-assignment.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; -import { makeRoleplayEnv, roleplayRequest, handleRoleplayEdgeRequest, withGlobalFetch, completionResponse } from "./helpers/roleplay_fixture.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { makeRoleplayEnv as rawMakeRoleplayEnv, roleplayRequest, handleRoleplayEdgeRequest, withGlobalFetch, completionResponse } from "./helpers/roleplay_fixture.mjs"; + +const makeRoleplayEnv = overrides => rawMakeRoleplayEnv(policyFixtureEnv(overrides)); const route = "/codex-easy/v1/chat/completions"; const authority = { route, provider: "codex-easy", principal: { id: "admin", keyHash: "a".repeat(64) } }; @@ -87,8 +98,10 @@ test("observability alone flushes content-free native observations through waitU const waits = [], exported = [], env = { OBSERVABILITY_EXPORTERS_JSON: JSON.stringify([{ type: "langfuse", endpoint: "https://collector.invalid/ingest", allowed_origins: ["https://collector.invalid"], credential_env: "FIXTURE_EXPORT_CREDENTIAL" }]), FIXTURE_EXPORT_CREDENTIAL: "synthetic" }; const exporter = new ObservabilityExporter(env, { transport: async (_url, options) => { exported.push(options.body); return new Response(null, { status: 200 }); } }); - setObservabilityExporter(env, exporter); - const res = await nativeGenerationFetch(request(), env, { waitUntil: promise => waits.push(promise) }, authority, response); + const guardedEnv = policyFixtureEnv(env); + setObservabilityExporter(guardedEnv, exporter); + const res = await rawNativeGenerationFetch(request(), guardedEnv, { waitUntil: promise => waits.push(promise) }, + { authenticated: true, ...authority }, response); assert.equal(await res.text(), complete); await Promise.all(waits); assert.equal(exported.length, 1); assert.doesNotMatch(exported[0], /Explain caching|fixture|placeholder/); assert.equal(exporter.status().accepted, 1); @@ -121,7 +134,7 @@ test("semantic maintenance uses at most three bounded pages and is off by defaul function durableFixture(t) { const sql = new DatabaseSync(":memory:"); t.after(() => sql.close()); - for (const name of ["0003_control_users", "0005_auto_routes", "0007_usage_ledger", "0019_generation_cache", + for (const name of ["0003_control_users", "0005_auto_routes", "0007_usage_ledger", "0041_key_data_guards", "0042_usage_token_details", "0019_generation_cache", "0020_usage_reservations", "0024_context_pages", "0025_canary_traffic", "0026_semantic_cache"]) { sql.exec(readFileSync(new URL(`../intelligence-migrations/${name}.sql`, import.meta.url), "utf8")); } diff --git a/tests/test_wave6_worker.mjs b/tests/test_wave6_worker.mjs index dbf4db0f..1f2c9b3b 100644 --- a/tests/test_wave6_worker.mjs +++ b/tests/test_wave6_worker.mjs @@ -1,14 +1,23 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; import { readFile } from "node:fs/promises"; -import { nativeGenerationFetch, withForwardedCorrelation } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch, withForwardedCorrelation } from "../worker/gateway-extensions.mjs"; import { ObservationWindow } from "../worker/latency-slo.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; import { DatabaseSync } from "node:sqlite"; import { handleReservationsRequest } from "../worker/reservations-d1.mjs"; import { activeUsersByPrefix } from "../worker/control-users-d1.mjs"; import { renderNativeMetrics } from "../worker/request-telemetry.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; const route = "/codex-easy/v1/chat/completions", now = Date.now(); const auth = { authenticated: true, route, provider: "codex-easy", keyId: "admin", principal: { id: "admin" } }; @@ -46,7 +55,7 @@ test("disabled native streaming and nonstream bodies, headers and account reads for (const streaming of [false, true]) { const upstream = streaming ? source([delta("ok"), "data: [DONE]\n\n"]).response : complete("ok"); const expected = await upstream.clone().text(), headers = [...upstream.headers]; - const res = await nativeGenerationFetch(request({ stream: streaming }), { INTELLIGENCE_DB: { prepare() { assert.fail("storage"); } } }, {}, auth, + const res = await nativeGenerationFetch(request({ stream: streaming }), { INTELLIGENCE_DB: { prepare(sql) { if (sql.startsWith("SELECT official_only")) return { bind() { return { first: async () => null }; } }; assert.fail("storage"); } } }, {}, auth, async req => { assert.deepEqual(await req.json(), { ...payload, stream: streaming }); return upstream; }); assert.equal(await res.text(), expected); assert.deepEqual([...res.headers], headers); } @@ -64,7 +73,7 @@ test("latency rejection runs before admission, cache, reservation, canary and di const store = new ObservationWindow(); store.record("codex-easy:m", { ttft_ms: 500, tokens_per_second: 1, now }); const env = { ...slo(), ...cost, ...canary("block"), USAGE_RESERVATIONS_ENABLED: "true", ADMISSION_ENABLED: "true", GENERATION_CACHE_SHARED_ENABLED: "true", GENERATION_CACHE_BACKEND: "d1-r2", SEMANTIC_CACHE_ENABLED: "true", - INTELLIGENCE_DB: { prepare() { assert.fail("storage before rejection"); } }, + INTELLIGENCE_DB: { prepare(sql) { if (sql.startsWith("SELECT official_only")) return { bind() { return { first: async () => null }; } }; assert.fail("storage before rejection"); } }, ADMISSION_COORDINATOR: { getByName() { assert.fail("admission before rejection"); } } }; const response = await nativeGenerationFetch(request(), env, {}, { ...auth, observations: store, now }, () => assert.fail("upstream")); assert.equal(response.status, 503); assert.equal((await response.json()).error.code, "latency_slo_predicted_miss"); @@ -92,6 +101,7 @@ test("capped native streams reject missing pricing before upstream", async () => test("a capped oversized streaming payload cannot bypass preflight inspection", async () => { const req = request({ stream: true, messages: [{ role: "user", content: "x".repeat(65536) }] }); const response = await nativeGenerationFetch(req, { ...cost, INTELLIGENCE_DB: { prepare(sql) { + if (sql.startsWith("SELECT official_only")) return { bind() { return { first: async () => null }; } }; assert.match(sql, /SELECT max_stream_cost_microusd/); return { bind() { return this; }, first() { return { max_stream_cost_microusd: 10 }; } }; } } }, {}, auth, () => assert.fail("upstream before bounded capped preflight")); @@ -110,7 +120,8 @@ test("stream cost stop delivers one error and one cancel and retains ambiguous a test("cost policy leaves nonstream malformed payloads alone and classifies stopped metrics before counting", async () => { const malformed = new Request("https://provider.invalid/v1/chat/completions", { method: "POST", body: "fixture-invalid" }); - const passthrough = await nativeGenerationFetch(malformed, cost, {}, auth, async req => { + const passthrough = await nativeGenerationFetch(malformed, cost, {}, { ...auth, + principal: { ...auth.principal, max_stream_cost_microusd: null } }, async req => { assert.equal(await req.text(), "fixture-invalid"); return complete("ok"); }); assert.equal(passthrough.status, 200); await passthrough.text(); @@ -129,7 +140,7 @@ for (const mode of ["log", "block"]) test("native canary " + mode + " scans befo try { const response = await nativeGenerationFetch(request({ temperature: 0 }, { "X-MultiLLM-Cache": "on" }), { ...canary(mode), GENERATION_CACHE_SHARED_ENABLED: "true", GENERATION_CACHE_BACKEND: "d1-r2", SEMANTIC_CACHE_ENABLED: "true", - INTELLIGENCE_DB: { prepare() { assert.fail("cache storage"); } } }, {}, auth, async req => { + INTELLIGENCE_DB: { prepare(sql) { if (sql.startsWith("SELECT official_only")) return { bind() { return { first: async () => null }; } }; assert.fail("cache storage"); } } }, {}, auth, async req => { const body = await req.json(); marker = body.messages[0].content.match(/marker: ([a-f0-9]{32})/)[1]; return complete("before " + marker + " after"); }, events(seen)); @@ -245,7 +256,7 @@ function sqliteDatabase(sql, observe = () => {}) { test("combined policies meter before settlement and retain a hold when the cost stop wins", async t => { const sql = new DatabaseSync(":memory:"); t.after(() => sql.close()); - for (const file of ["0003_control_users.sql", "0007_usage_ledger.sql", "0020_usage_reservations.sql"]) + for (const file of ["0003_control_users.sql", "0007_usage_ledger.sql", "0041_key_data_guards.sql", "0042_usage_token_details.sql", "0020_usage_reservations.sql"]) sql.exec(await readFile(new URL("../intelligence-migrations/" + file, import.meta.url), "utf8")); const order = [], seen = [], store = new ObservationWindow(); store.record("codex-easy:m", { ttft_ms: 1, tokens_per_second: 1000, now }); diff --git a/tests/test_wave7_worker.mjs b/tests/test_wave7_worker.mjs index 7ee50c45..fe5d9896 100644 --- a/tests/test_wave7_worker.mjs +++ b/tests/test_wave7_worker.mjs @@ -1,3 +1,12 @@ +import { policyFixtureEnv } from "./helpers/dispatch_fixture.mjs"; +const nativeGenerationFetch = (request, env, ctx, authority, ...rest) => rawNativeGenerationFetch( + request, policyFixtureEnv(env), ctx, { authenticated: true, ...authority }, ...rest); +async function loadWorkerModule(...args) { + const module = await rawLoadWorkerModule(...args); + return { ...module, default: { ...module.default, fetch(request, env, ctx) { + return module.default.fetch(request, policyFixtureEnv(env), ctx); + } } }; +} import assert from "node:assert/strict"; import test from "node:test"; import { DatabaseSync } from "node:sqlite"; @@ -7,7 +16,7 @@ import { createCreditsLifecycle, CreditsAdmissionError } from "../worker/credits import { appendCredit, readCredits, contextOwner } from "../worker/credits-d1.mjs"; import { handleTenantGovernanceRequest } from "../worker/tenant-governance-d1.mjs"; import { handleReservationsRequest } from "../worker/reservations-d1.mjs"; -import { nativeGenerationFetch, nativeGenerationSetup } from "../worker/gateway-extensions.mjs"; +import { nativeGenerationFetch as rawNativeGenerationFetch, nativeGenerationSetup } from "../worker/gateway-extensions.mjs"; import { exactCacheIdentity } from "../worker/exact-generation-cache.mjs"; import { semanticPartition } from "../worker/semantic-generation-cache.mjs"; import { handleIntelligenceOutbound } from "../worker/intelligence-outbound.mjs"; @@ -18,7 +27,7 @@ import { recordNativeUsage } from "../worker/usage-ledger-d1.mjs"; import { tenantStorageKey } from "../worker/tenants-d1.mjs"; import { retentionRequestId } from "../worker/retention-policy.mjs"; import { USER_FIELDS } from "../worker/control-users-d1.mjs"; -import { loadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; +import { loadWorkerModule as rawLoadWorkerModule } from "./helpers/load_cloudflare_worker.mjs"; import { handleManagedStateRequest } from "../worker/managed-state-dispatch.mjs"; const request = path => new Request(`http://intelligence.internal${path}`, { @@ -48,7 +57,7 @@ function fixture(t, flags = {}) { for (const migration of ["0003_control_users.sql", "0007_usage_ledger.sql", "0011_secret_firewall.sql", "0013_shadow_eval.sql", "0017_control_revisions.sql", "0019_generation_cache.sql", "0020_usage_reservations.sql", "0023_idempotency.sql", "0024_context_pages.sql", "0026_semantic_cache.sql", "0028_responses_state.sql", "0033_tenant_hierarchy.sql", "0034_tenant_governance.sql", - "0035_saml_federation.sql", "0036_scim_provisioning.sql", "0037_credits_ledger.sql", "0038_payment_billing.sql"]) + "0035_saml_federation.sql", "0036_scim_provisioning.sql", "0037_credits_ledger.sql", "0038_payment_billing.sql", "0041_key_data_guards.sql", "0042_usage_token_details.sql"]) sql.exec(readFileSync(new URL(`../intelligence-migrations/${migration}`, import.meta.url), "utf8")); const operations = []; let failBatch = false; diff --git a/worker/access-sso.mjs b/worker/access-sso.mjs index 0746a0d1..931215f9 100644 --- a/worker/access-sso.mjs +++ b/worker/access-sso.mjs @@ -1,3 +1,4 @@ +import { controlNetwork } from "./dispatch-network.mjs"; /** * Cloudflare Access single sign-on for the dashboard. On the sign-in path only, the Worker * verifies the Access application token (RS256 against the team's published keys, then @@ -120,9 +121,10 @@ async function boundedText(response, maxBytes) { } async function fetchSigningKeys(teamDomain, fetchImpl) { - const response = await fetchImpl(`${teamDomain}/cdn-cgi/access/certs`, { + const url = `${teamDomain}/cdn-cgi/access/certs`; + const response = await controlNetwork(url, () => fetchImpl(url, { headers: { accept: "application/json" }, redirect: "manual", signal: AbortSignal.timeout(KEYS_TIMEOUT_MS), - }); + })); if (response.status !== 200) { void response.body?.cancel().catch(() => {}); throw new Error(`Access keys answered HTTP ${response.status}`); diff --git a/worker/account-contract.mjs b/worker/account-contract.mjs new file mode 100644 index 00000000..11f22464 --- /dev/null +++ b/worker/account-contract.mjs @@ -0,0 +1,31 @@ +/** Negotiate additive account metadata before crossing a strict Container decoder. */ +import { verifyDataPolicy } from "./dispatch-policy.mjs"; + +export const ACCOUNT_POLICY_VERSION = 1; +const POLICY_FIELDS = new Set(["official_only", "forced_provider_preferences", "data_policy"]); + +export function accountPolicyVersion(body) { + if (!Object.hasOwn(body, "data_policy_version")) return null; + if (body.data_policy_version !== ACCOUNT_POLICY_VERSION) throw new Error("Unsupported account policy version"); + delete body.data_policy_version; + return ACCOUNT_POLICY_VERSION; +} + +export function hasAccountPolicy(row) { + return Boolean(row?.official_only) || row?.forced_provider_preferences != null; +} + +export function accountReply(row, version) { + if (!row || version === ACCOUNT_POLICY_VERSION) return row; + // An older Container cannot enforce these controls. Never turn them into a + // legacy unrestricted account, even during a rolling deployment. + verifyDataPolicy(row.data_policy); + if (hasAccountPolicy(row)) throw new Error("Account policy requires a current Container"); + return Object.fromEntries(Object.entries(row).filter(([name]) => !POLICY_FIELDS.has(name))); +} + +export function accountWrite(user, version) { + if (version === ACCOUNT_POLICY_VERSION || !user || typeof user !== "object" || Array.isArray(user)) return user; + if ([...POLICY_FIELDS].some(name => Object.hasOwn(user, name))) throw new Error("Account policy requires negotiation"); + return { ...user, official_only: null, forced_provider_preferences: null }; +} diff --git a/worker/ai-outbound.mjs b/worker/ai-outbound.mjs index 63dadcaa..a6ba5e11 100644 --- a/worker/ai-outbound.mjs +++ b/worker/ai-outbound.mjs @@ -1,3 +1,4 @@ +import { bindingPrincipal, withDispatchPrincipal, providerFetch, providerOperation, dispatchFailure, DispatchError } from "./provider-dispatch.mjs"; /** * Cloudflare AI for the Container, reachable only through its private outbound handler * (`http://ai.internal`). Third-party models (OpenAI GPT Image, Google Veo) run through @@ -94,7 +95,7 @@ async function imageData(image, responseFormat) { if (typeof image !== "string" || !image) return null; if (/^https:\/\//i.test(image)) { if (responseFormat === "url") return { url: image }; - const response = await fetch(image, { signal: AbortSignal.timeout(30000) }); + const response = await providerFetch("cloudflare", image, { signal: AbortSignal.timeout(30000) }); const length = Number(response.headers.get("content-length") ?? 0); if (!response.ok || length > MAX_FETCHED_IMAGE_BYTES) throw new Error(`Image download failed with HTTP ${response.status}`); const bytes = await response.arrayBuffer(); @@ -106,7 +107,7 @@ async function imageData(image, responseFormat) { } async function run(env, model, input) { - const response = await env.AI.run(model, input, { gateway: { id: env.AI_GATEWAY_ID || "default" } }); + const response = await providerOperation("cloudflare", () => env.AI.run(model, input, { gateway: { id: env.AI_GATEWAY_ID || "default" } })); return isRecord(response?.result) ? response.result : response; } @@ -158,7 +159,7 @@ async function speech(env, body) { if (!encoding) return failure("invalid_request", "Unsupported response_format.", 400); const input = { text: body.input, speaker: AURA_SPEAKERS.has(body.voice) ? body.voice : "luna", encoding, ...(container ? { container } : {}) }; - const result = await env.AI.run(body.model, input, { gateway: { id: env.AI_GATEWAY_ID || "default" } }); + const result = await providerOperation("cloudflare", () => env.AI.run(body.model, input, { gateway: { id: env.AI_GATEWAY_ID || "default" } })); let audio = null; if (result instanceof ReadableStream) audio = result; else if (result instanceof Response) audio = result.body; @@ -195,7 +196,7 @@ async function generateVideo(env, body) { return Response.json({ status: "completed", video_url: result.video, model: body.model }, { headers: { "cache-control": "no-store" } }); } -export async function handleAiOutbound(request, env) { +async function runAiOutbound(request, env) { const url = new URL(request.url); if (url.origin !== "http://ai.internal" || url.search || url.hash || url.username || url.password || request.method !== "POST") { return failure("not_found", "Unknown Cloudflare AI operation.", 404); @@ -222,8 +223,15 @@ export async function handleAiOutbound(request, env) { if (url.pathname === "/v1/videos/generations") return await generateVideo(env, body); return failure("not_found", "Unknown Cloudflare AI operation.", 404); } catch (error) { + if (error instanceof DispatchError) return dispatchFailure(error); const [code, status] = refusal(error); logFailure("cloudflare_ai_failed", error, { model: typeof body.model === "string" ? body.model.slice(0, 80) : "unknown", code }); return failure(code, "Cloudflare AI could not complete the generation.", status); } } + +export async function handleAiOutbound(request, env) { + if (new URL(request.url).origin !== "http://ai.internal") return failure("not_found", "Unknown Cloudflare AI operation.", 404); + try { return await withDispatchPrincipal(env, bindingPrincipal(request), () => runAiOutbound(request, env)); } + catch (error) { return dispatchFailure(error); } +} diff --git a/worker/container-env.mjs b/worker/container-env.mjs index 6687e4fa..d18abb2a 100644 --- a/worker/container-env.mjs +++ b/worker/container-env.mjs @@ -113,6 +113,7 @@ const DIRECT_ENV_KEYS = [ "ADMISSION_AUTHORITY_URL", "FREE_ROUTE_EXTRA_PROVIDERS", "FREE_ROUTE_FREE_TIER_PROVIDERS", + "GEMINI_DATA_TIER", "FREE_ROUTE_PROVIDER_ORDER", "FREE_ROUTE_WORKERSAI_ACCOUNT_ID", "GROQ_API_KEY", diff --git a/worker/control-users-d1.mjs b/worker/control-users-d1.mjs index 530d7d62..4270407d 100644 --- a/worker/control-users-d1.mjs +++ b/worker/control-users-d1.mjs @@ -2,6 +2,9 @@ * Durable dashboard accounts (username, key hash and metadata) in D1, reachable only * through the Container's private outbound handler. Fixed statements, no client SQL. */ +import { dataPolicy } from "./dispatch-policy.mjs"; +import { accountPolicyVersion, accountReply, accountWrite } from "./account-contract.mjs"; +import { validDataControls } from "./key-data-guards.mjs"; import { STREAM_CAP_FIELD, streamCostEnabled, validStreamCap } from "./stream-cost-breaker.mjs"; import { commitRevision, revisionSyncSettings } from "./config-revision.mjs"; import { logFailure } from "./log.mjs"; @@ -9,7 +12,7 @@ import { AUDIT_OPERATIONS, handleAuditOperation } from "./control-audit-d1.mjs"; export const USER_FIELDS = Object.freeze(["username", "api_key_hash", "api_key_prefix", "scopes", "is_admin", "created_at", "last_login", "last_used_at", "last_used_ip", "created_by", "rotated_at", "revoked_at", - "daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate"]); + "daily_budget_usd", "monthly_budget_usd", "allowed_models", "allowed_ips", "expires_at", "secret_scan_mode", "shadow_eval_rate", "official_only", "forced_provider_preferences"]); // Migration 0007 added the per-key controls. Until it is applied, reads return the // older columns with no controls, so deploying before migrating cannot lock keys out. const CONTROL_FIELDS = USER_FIELDS.slice(12); @@ -42,6 +45,9 @@ export function validUser(user) { if (!validStreamCap(user[STREAM_CAP_FIELD])) return false; user = Object.fromEntries(Object.entries(user).filter(([name]) => name !== STREAM_CAP_FIELD)); } + if (user && Object.hasOwn(user, "data_policy")) { + user = Object.fromEntries(Object.entries(user).filter(([name]) => name !== "data_policy")); + } if (user && !Object.hasOwn(user, "shadow_eval_rate")) user = { ...user, shadow_eval_rate: null }; if (user && !Object.hasOwn(user, "secret_scan_mode")) user = { ...user, secret_scan_mode: null }; return user !== null && typeof user === "object" && !Array.isArray(user) && fields(user, USER_FIELDS) @@ -53,6 +59,8 @@ export function validUser(user) { && (user.secret_scan_mode === null || ["off", "observe", "redact", "block"].includes(user.secret_scan_mode)) && (user.shadow_eval_rate === null || (typeof user.shadow_eval_rate === "number" && Number.isFinite(user.shadow_eval_rate) && user.shadow_eval_rate >= 0 && user.shadow_eval_rate <= 0.2)) + && validDataControls(user) + && (user.forced_provider_preferences === null || typeof user.forced_provider_preferences === "string") && budget(user.daily_budget_usd) && budget(user.monthly_budget_usd) && list(user.allowed_models, MODEL_PATTERNS) && list(user.allowed_ips, IP_RANGES) && (user.expires_at === null || (text(user.expires_at, 64) && Number.isFinite(Date.parse(user.expires_at)))); @@ -139,11 +147,16 @@ async function read(query) { /** Run an account query with every column, or with the pre-0007 columns and no controls. */ async function readUsers(query, env = {}) { - if (streamCostEnabled(env)) return read(() => query(streamUserFields(env).join(", "))); - for (const end of [USER_FIELDS.length, USER_FIELDS.length - 1, USER_FIELDS.length - 2, 12]) { + const stamp = row => row && { ...row, data_policy: dataPolicy(row) }; + if (streamCostEnabled(env) || revisionSyncSettings(env).enabled) { + const rows = await read(() => query(streamUserFields(env).join(", "))); + return Array.isArray(rows) ? rows.map(stamp) : stamp(rows); + } + for (const end of [USER_FIELDS.length, USER_FIELDS.length - 1, USER_FIELDS.length - 2, USER_FIELDS.length - 3, USER_FIELDS.length - 4, 12]) { try { const rows = await read(() => query(USER_FIELDS.slice(0, end).join(", "))); - const defaults = row => row && { ...Object.fromEntries(USER_FIELDS.slice(end).map(name => [name, null])), ...row }; + const defaults = row => row && { ...Object.fromEntries(USER_FIELDS.slice(end).map(name => [name, null])), ...row, + data_policy: end === USER_FIELDS.length ? dataPolicy(row) : null }; return Array.isArray(rows) ? rows.map(defaults) : defaults(rows); } catch (error) { if (!missingColumn(error) || end === 12) throw error; @@ -206,6 +219,9 @@ export async function handleControlUsersRequest(request, env) { if (!body || Array.isArray(body) || typeof body !== "object" || body.version !== 1) return reply({ error: "invalid_request" }, 400); } catch { return reply({ error: "invalid_request" }, 400); } const db = env.INTELLIGENCE_DB; + let policyVersion; + try { policyVersion = accountPolicyVersion(body); } + catch { return reply({ error: "invalid_request" }, 400); } try { if (AUDIT_OPERATIONS.has(body.operation)) { const result = await handleAuditOperation(db, body); @@ -219,23 +235,30 @@ export async function handleControlUsersRequest(request, env) { ? db.prepare(`SELECT ${columns} FROM control_users ORDER BY username LIMIT ?`).bind(body.limit) : db.prepare(`SELECT ${columns} FROM control_users WHERE username > ? ORDER BY username LIMIT ?`).bind(body.after, body.limit) ).all()).results; - const users = revisionSyncSettings(env).enabled ? await read(() => query(streamUserFields(env).join(", "))) : await readUsers(query, env); - return reply({ version: 1, users }); + const users = await readUsers(query, env); + return reply({ version: 1, users: users.map(user => accountReply(user, policyVersion)) }); } case "get": { if (!fields(body, ["version", "operation", "username"]) || !text(body.username, 128)) break; const user = await readUsers(columns => db.prepare(`SELECT ${columns} FROM control_users WHERE username=?`) .bind(body.username).first(), env); - return reply({ version: 1, user: user ?? null }); + return reply({ version: 1, user: accountReply(user ?? null, policyVersion) }); } case "by_prefix": { if (!fields(body, ["version", "operation", "prefix"]) || !text(body.prefix, 64)) break; - return reply({ version: 1, users: await activeUsersByPrefix(db, body.prefix, env) }); + return reply({ version: 1, users: (await activeUsersByPrefix(db, body.prefix, env)).map(user => accountReply(user, policyVersion)) }); } case "upsert": { - if (!fields(body, ["version", "operation", "user"]) || !validUser(body.user)) break; + if (!fields(body, ["version", "operation", "user"])) break; + body.user = accountWrite(body.user, policyVersion); + if (!validUser(body.user)) break; + if (policyVersion === null) { + const prior = await readUsers(columns => db.prepare(`SELECT ${columns} FROM control_users WHERE username=?`) + .bind(body.user.username).first(), env); + if (prior) accountReply(prior, policyVersion); + } if (!streamCostEnabled(env) && body.user[STREAM_CAP_FIELD] != null) return reply({ error: "invalid_request" }, 400); - const user = { shadow_eval_rate: null, secret_scan_mode: null, ...body.user }; + const user = { official_only: null, forced_provider_preferences: null, shadow_eval_rate: null, secret_scan_mode: null, ...body.user }; if (grantsAdmin(user) && !adminUsernames(env).has(user.username)) { logFailure("account_admin_refused", new Error("Administration is not configured for this username")); await audit(db, "upsert", "refused", user).run(); @@ -252,7 +275,7 @@ export async function handleControlUsersRequest(request, env) { return reply({ version: 1, stored: true }); } // The write and its audit row commit together, or neither does. - for (const end of [USER_FIELDS.length, USER_FIELDS.length - 1, USER_FIELDS.length - 2, 12]) { + for (const end of [USER_FIELDS.length, USER_FIELDS.length - 1, USER_FIELDS.length - 2, USER_FIELDS.length - 3, USER_FIELDS.length - 4, 12]) { if (USER_FIELDS.slice(end).some(name => user[name] !== null)) throw new Error("Account controls require migration"); try { const statements = [upsertStatement(db, user, USER_FIELDS.slice(0, end)), audit(db, "upsert", "stored", user)]; diff --git a/worker/dispatch-network.mjs b/worker/dispatch-network.mjs new file mode 100644 index 00000000..2aa5821b --- /dev/null +++ b/worker/dispatch-network.mjs @@ -0,0 +1,32 @@ +/** Network capabilities last only for one admitted transport operation. */ +import { AsyncLocalStorage } from "node:async_hooks"; +const network = new AsyncLocalStorage(); +export const networkAdmitted = input => { + const scope = network.getStore(); + if (!scope?.active) return false; + if (!scope.url) return true; + if (!input) return false; + try { return new URL(typeof input === "string" || input instanceof URL ? input : input.url, "https://gateway.fixture").href === scope.url; } + catch { return false; } +}; +export const connectionAdmitted = (host, port) => { + const scope = network.getStore(); + if (!scope?.active) return false; + if (!scope.url) return true; + const target = new URL(scope.url); + return host === target.hostname && Number(port) === Number(target.port || (target.protocol === "https:" ? 443 : 80)); +}; +function scoped(scope, run) { + return network.run(scope, () => { + try { + const result = run(); + if (result && typeof result.then === "function") return Promise.resolve(result).finally(() => { scope.active = false; }); + scope.active = false; + return result; + } catch (error) { scope.active = false; throw error; } + }); +} +export const admittedNetwork = run => scoped({ active: true }, run); +/** Certificate discovery, configured content-free alerts and browser test clients + * are control traffic. This scope authorizes only their exact destination. */ +export const controlNetwork = (url, run) => scoped({ active: true, url: new URL(url, "https://gateway.fixture").href }, run); diff --git a/worker/dispatch-policy.mjs b/worker/dispatch-policy.mjs new file mode 100644 index 00000000..ca37f86a --- /dev/null +++ b/worker/dispatch-policy.mjs @@ -0,0 +1,14 @@ +/** A supported version and both explicit fields are mandatory, including nulls. */ +import { parsePreferences, validDataControls } from "./key-data-guards.mjs"; + +export function dataPolicy(user) { + if (!validDataControls(user)) throw new TypeError("Unverifiable data policy"); + return { version: 1, official_only: user.official_only, + forced_provider_preferences: parsePreferences(user.forced_provider_preferences) }; +} +export function verifyDataPolicy(policy) { + if (!policy || policy.version !== 1 || Object.keys(policy).sort().join() !== "forced_provider_preferences,official_only,version") { + throw new TypeError("Unverifiable data policy"); + } + return dataPolicy(policy); +} diff --git a/worker/gateway-extensions.mjs b/worker/gateway-extensions.mjs index 93c39ad0..f5963ce6 100644 --- a/worker/gateway-extensions.mjs +++ b/worker/gateway-extensions.mjs @@ -1,3 +1,5 @@ +import { DispatchError, providerFetch, withDispatchPrincipal, dispatchFailure } from "./provider-dispatch.mjs"; +import { guardNativeRequest } from "./key-data-guards.mjs"; import { governanceEnabled, GovernanceError } from "./tenant-governance-d1.mjs"; import { creditsEnforcement } from "./credits-d1.mjs"; import { CreditsAdmissionError } from "./credits-admission.mjs"; @@ -332,7 +334,20 @@ async function runNativeLifecycle(request, env, ctx, authority, fetcher, registr } export async function nativeGenerationFetch(request, env, ctx, authority, fetcher, collaborators = []) { + const send = fetcher; + fetcher = async (outbound, runtime, current) => { + try { return await providerFetch(current.provider, outbound, {}, { env: runtime, authority: current, + send: checked => send(checked, runtime, current) }); } + catch (error) { if (!(error instanceof DispatchError)) throw error; return dispatchFailure(error); } + }; + return withDispatchPrincipal(env, authority, () => runNativeGenerationFetch(request, env, ctx, authority, fetcher, collaborators)); +} + +async function runNativeGenerationFetch(request, env, ctx, authority, fetcher, collaborators = []) { const path = authority.route ?? new URL(request.url).pathname; + const guarded = await guardNativeRequest(request, env, authority); + if (guarded.response) { authority.deadlineHook?.deadline?.stop(); return guarded.response; } + request = guarded.request; if (!generationPath(request, path)) return fetcher(request, env, authority); let enterprise = authority.authenticated === true && (governanceEnabled(env) || creditsEnforcement(env) !== "off"); if ((organisationsEnabled(env) || enterprise) && authority.authenticated === true) { diff --git a/worker/intelligence-auth-d1.mjs b/worker/intelligence-auth-d1.mjs index 79020c11..b4133a2c 100644 --- a/worker/intelligence-auth-d1.mjs +++ b/worker/intelligence-auth-d1.mjs @@ -1,3 +1,4 @@ +import { dataPolicy } from "./dispatch-policy.mjs"; import { logFailure } from "./log.mjs"; export const INTEGRATION_SCOPES = Object.freeze(["chat", "models", "audio", "embeddings", @@ -74,6 +75,14 @@ export async function handleIntelligenceAuthRequest(request, env) { return reply({version: 1, principal: await lookupIntegrationPrincipal(db, body.keyPrefix)}); } if (typeof body.principalId !== "string" || !idPattern.test(body.principalId)) return reply({error: "invalid_request"}, 400); + if (body.operation === "dispatch_policy") { + if (!fields(body, ["version", "operation", "principalId"])) return reply({error: "invalid_request"}, 400); + const row = await db.prepare(`SELECT p.official_only, p.forced_provider_preferences, c.key_prefix + FROM intelligence_principals p JOIN intelligence_credentials c ON c.principal_id=p.id AND c.version=p.version + WHERE p.id=? AND p.revoked_at IS NULL`).bind(body.principalId).first(); + return reply({version: 1, principal: row ? { username: body.principalId, api_key_prefix: row.key_prefix, + data_policy: dataPolicy(row) } : null}); + } const now = new Date().toISOString(); if (body.operation === "provision") { if (!fields(body, ["version", "operation", "principalId", "keyPrefix", "keyHash", "scopes"]) diff --git a/worker/key-data-guards.mjs b/worker/key-data-guards.mjs new file mode 100644 index 00000000..d17ecaed --- /dev/null +++ b/worker/key-data-guards.mjs @@ -0,0 +1,75 @@ +/** Shared provider classes and administrator-owned preferences at native dispatch. */ +import classes from "../providers/data-classes.json" with { type: "json" }; + +const object = value => value !== null && typeof value === "object" && !Array.isArray(value); +export function parsePreferences(value) { + if (value == null) return null; + const parsed = typeof value === "string" ? JSON.parse(value) : value; + if (!object(parsed) || new TextEncoder().encode(JSON.stringify(parsed)).length > 4096 + || (Object.hasOwn(parsed, "data_collection") && !["allow", "deny"].includes(parsed.data_collection))) { + throw new TypeError("Invalid provider preferences"); + } + return parsed; +} + +export function validDataControls(user) { + try { + if (!user || !Object.hasOwn(user, "official_only") || !Object.hasOwn(user, "forced_provider_preferences") + || user.official_only === undefined || user.forced_provider_preferences === undefined) return false; + parsePreferences(user.forced_provider_preferences); + return user.official_only === null || [0, 1].includes(user.official_only); + } catch { return false; } +} + +export function providerDataClass(provider, env = {}) { + provider = String(provider).trim().toLowerCase(); + if (provider === "free" || String(env.FREE_ROUTE_FREE_TIER_PROVIDERS ?? "").split(/[\s,]+/).includes(provider)) return "free"; + if (provider === "gemini") return env.GEMINI_DATA_TIER === "paid" ? "official" : "free"; + return Object.hasOwn(classes, provider) ? classes[provider] : provider.startsWith("ce-") ? "subscription" : "reseller"; +} + +export function providerClassPermitted(user, provider, env = {}) { + return !user?.official_only || providerDataClass(provider, env) === "official"; +} + +export function mergePreferences(caller, forced) { + const entries = new Map(Object.entries(object(caller) ? caller : {})); + for (const [name, value] of Object.entries(forced)) { + if (name === "data_collection" && entries.get(name) === "deny") continue; + entries.set(name, object(value) ? mergePreferences(entries.get(name), value) : structuredClone(value)); + } + return Object.fromEntries(entries); +} + +/** Read current administrator controls without accepting policy from client headers. */ +export async function currentDataControls(env, authority) { + const id = authority.keyId ?? authority.principal?.owner ?? authority.principal?.id; + if (!env.INTELLIGENCE_DB || !id) throw new TypeError("Unverifiable authority"); + const user = id.startsWith("integration:") + ? await env.INTELLIGENCE_DB.prepare("SELECT official_only, forced_provider_preferences FROM intelligence_principals WHERE id = ? AND revoked_at IS NULL").bind(id).first() + : await env.INTELLIGENCE_DB.prepare( + "SELECT official_only, forced_provider_preferences FROM control_users WHERE username = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)") + .bind(id, new Date().toISOString()).first(); + if (!validDataControls(user)) throw new TypeError("Unverifiable controls"); + return user; +} + +/** Resolve controls from trusted authority or durable accounts, never client headers. */ +export async function guardNativeRequest(request, env, authority) { + if (authority.authenticated !== true) return { response: Response.json({ error: { code: "key_data_controls_unavailable" } }, { status: 503 }) }; + try { + const user = await currentDataControls(env, authority); + if (!providerClassPermitted(user, authority.provider, env)) { + return { response: Response.json({ error: { code: "provider_class_not_allowed" } }, { status: 403 }) }; + } + const forced = parsePreferences(user.forced_provider_preferences); + if (authority.provider !== "openrouter" || !forced || !request.body) return { request }; + const payload = await request.clone().json(); + if (!object(payload)) return { response: Response.json({ error: { code: "invalid_request" } }, { status: 400 }) }; + payload.provider = mergePreferences(payload.provider, forced); + const headers = new Headers(request.headers); headers.delete("content-length"); + return { request: new Request(request, { headers, body: JSON.stringify(payload) }) }; + } catch { + return { response: Response.json({ error: { code: "key_data_controls_unavailable" } }, { status: 503 }) }; + } +} diff --git a/worker/knowledge-edge.mjs b/worker/knowledge-edge.mjs index d5863ea9..ab1c4845 100644 --- a/worker/knowledge-edge.mjs +++ b/worker/knowledge-edge.mjs @@ -288,7 +288,7 @@ async function dispatch(env, operation, principal, payload, signal) { let result; try { response = await env.KNOWLEDGE_SERVICE.fetch("http://knowledge.internal/v1/dispatch", { - method: "POST", headers: { "content-type": "application/json" }, body, + method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": JSON.stringify({ version: 1, username: principal.id }) }, body, signal: signal ? AbortSignal.any([signal, deadline]) : deadline, }); result = await response.json(); diff --git a/worker/knowledge-outbound.mjs b/worker/knowledge-outbound.mjs index 2ebed15a..8109bfce 100644 --- a/worker/knowledge-outbound.mjs +++ b/worker/knowledge-outbound.mjs @@ -1,3 +1,4 @@ +import { bindingPrincipal } from "./provider-dispatch.mjs"; import { SYNC_REQUEST_BYTES } from "./knowledge/skills-validation.mjs"; import { errorReply, fail, readJson } from "./knowledge/contracts.mjs"; @@ -7,10 +8,11 @@ export async function handleKnowledgeOutbound(request, env) { if (request.method !== "POST" || url.origin !== "http://knowledge.internal" || url.pathname !== "/v1/dispatch" || url.search || url.hash || url.username || url.password) fail("invalid_target", "Invalid Knowledge service target.", 400); if (!env.KNOWLEDGE_SERVICE) fail("knowledge_not_configured", "The private Knowledge service binding is not configured.", 503); + bindingPrincipal(request); const payload = await readJson(request, SYNC_REQUEST_BYTES); if (payload.operation !== "skills.sync" && new TextEncoder().encode(JSON.stringify(payload)).length > 65536) fail("request_too_large", "Request is too large.", 413); return env.KNOWLEDGE_SERVICE.fetch("http://knowledge.internal/v1/dispatch", { - method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), signal: request.signal, + method: "POST", headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": request.headers.get("X-MultiLLM-Dispatch-Principal") }, body: JSON.stringify(payload), signal: request.signal, }); } catch (error) { return errorReply(error); } } diff --git a/worker/knowledge/authority.mjs b/worker/knowledge/authority.mjs index c2a25607..8da966a5 100644 --- a/worker/knowledge/authority.mjs +++ b/worker/knowledge/authority.mjs @@ -61,7 +61,8 @@ async function evictDiscovery(tx, sources) { async function createSource(tx, input, now, discovered = false) { const policy = await policyOf(tx); - const parsed = parseSource(input, policy); + const { dispatch_principal, ...sourceInput } = input; + const parsed = parseSource(sourceInput, policy); const id = await digest(`${parsed.url}\0${parsed.product}\0${parsed.version}`); const existing = await tx.get(`source:${id}`); if (existing && (discovered || existing.identity_confirmed)) return existing; @@ -77,6 +78,7 @@ async function createSource(tx, input, now, discovered = false) { ? { ...existing, ...parsed, title: parsed.title || existing.title, identity_confirmed: true, revision: existing.revision + 1 } : { ...parsed, id, identity_confirmed: !discovered, revision: 1, fence: 0, current_artifact: null, created_at: new Date(now).toISOString(), last_checked_at: null }; + if (dispatch_principal?.authenticated === true && typeof dispatch_principal.keyId === "string") source.dispatch_principal = dispatch_principal; await tx.put(`source:${id}`, source); // A new discovery has no revision yet, so no cached answer or excerpt depends on it. // Bumping here made every parallel query that discovered a source fail with corpus_changed. @@ -138,7 +140,9 @@ async function enqueue(tx, input, now) { retainedByPolicy(artifact, await policyOf(tx)); Object.assign(job, { status: "snapshot", artifact_id: artifact.id, index_key: artifact.index_key }); } - await tx.put(`source:${source.id}`, { ...source, fence: job.fence }); + const principal = source.dispatch_principal ?? input.dispatch_principal; + if (principal?.authenticated === true && typeof principal.keyId === "string") job.dispatch_principal = principal; + await tx.put(`source:${source.id}`, { ...source, ...(job.dispatch_principal ? { dispatch_principal: job.dispatch_principal } : {}), fence: job.fence }); await tx.put(`job:${id}`, job); return job; } diff --git a/worker/knowledge/corpus.mjs b/worker/knowledge/corpus.mjs index 59d711e9..8e67d7a6 100644 --- a/worker/knowledge/corpus.mjs +++ b/worker/knowledge/corpus.mjs @@ -1,3 +1,4 @@ +import { providerOperation } from "../provider-dispatch.mjs"; const MAX_SNAPSHOT_BYTES = 256 * 1024; const MAX_INDEX_RESULTS = 20; const INDEX_PAGE_SIZE = 50; @@ -91,7 +92,7 @@ function itemInfo(value, key) { async function readIndex(callback) { for (let attempt = 0; ; attempt += 1) { - try { return await callback(); } + try { return await providerOperation("cloudflare", callback); } catch (error) { // The binding intermittently rejects its own account context before a metadata read. // Uploads, deletions and billable search requests never use this retry path. @@ -184,13 +185,13 @@ export class KnowledgeCorpus { if (typeof request.product === "string" && request.product && encoder.encode(request.product).byteLength <= 64) retrieval.filters = { product: request.product }; const response = this.searchAI ? await this.searchWithAI(request, retrieval.filters) - : await this.requireIndex().search({ + : await providerOperation("cloudflare", () => this.requireIndex().search({ query: request.query, ai_search_options: { retrieval, query_rewrite: { enabled: false }, reranking: { enabled: false }, cache: { enabled: false }, }, - }); + })); if (!response || !Array.isArray(response.chunks) || response.chunks.length > MAX_INDEX_RESULTS) { throw invalid("invalid_index_response"); } @@ -211,23 +212,23 @@ export class KnowledgeCorpus { } // Select one transport before dispatch. Never retry a billable query through // another binding when the first request has an unknown outcome. - const response = await this.searchAI.autorag(this.searchInstance).search({ + const response = await providerOperation("cloudflare", () => this.searchAI.autorag(this.searchInstance).search({ query: request.query, rewrite_query: false, max_num_results: MAX_INDEX_RESULTS, reranking: { enabled: false }, ...(filters ? { filters: { type: "eq", key: "product", value: filters.product } } : {}), - }); + })); return legacySearchChunks(response); } async uploadRevision(artifact, text) { await checkedBytes(artifact, text); - const result = await this.requireIndex().items.upload(artifact.index_key, text, { + const result = await providerOperation("cloudflare", () => this.requireIndex().items.upload(artifact.index_key, text, { metadata: { artifact_id: artifact.id, content_hash: artifact.content_hash, product: artifact.product, version_kind: artifact.version.kind, version: artifact.version.version ?? "unknown", }, - }); + })); return itemReference(result, artifact.index_key); } @@ -286,7 +287,7 @@ export class KnowledgeCorpus { if (artifact.item_id || this.index?.items) { const item = await this.reconcileRevision(artifact); if (item && artifact.item_id && item.id !== artifact.item_id) throw invalid("invalid_artifact_removal"); - if (item) await this.requireIndex().items.delete(item.id); + if (item) await providerOperation("cloudflare", () => this.requireIndex().items.delete(item.id)); } await this.requireBucket().delete(artifact.snapshot_key); } diff --git a/worker/knowledge/index.mjs b/worker/knowledge/index.mjs index 48642ec7..60bf3c33 100644 --- a/worker/knowledge/index.mjs +++ b/worker/knowledge/index.mjs @@ -1,3 +1,4 @@ +import { bindingPrincipal, withDispatchPrincipal } from "../provider-dispatch.mjs"; import { DurableObject, WorkflowEntrypoint } from "cloudflare:workers"; import { KnowledgeAuthority } from "./authority.mjs"; import { dispatchKnowledge, maintainKnowledge } from "./service.mjs"; @@ -73,6 +74,7 @@ export class KnowledgeHandoffs extends DurableObject { export class KnowledgeSkills extends DurableObject { constructor(ctx, env) { super(ctx, env); + this.env = env; this.skills = new SkillsStore(ctx.storage, env); } async fetch(request) { @@ -81,13 +83,13 @@ export class KnowledgeSkills extends DurableObject { const body = await readJson(request, SYNC_REQUEST_BYTES); fields(body, ["operation", "payload", "principal"], ["operation", "payload", "principal"]); if (typeof body.principal !== "string" || !body.principal || body.principal.length > 256) fail("invalid_principal", "Invalid skills principal.", 403); - return reply(await this.skills.call(body.operation, body.payload, body.principal)); + return await withDispatchPrincipal(this.env, bindingPrincipal(request), async () => reply(await this.skills.call(body.operation, body.payload, body.principal))); } catch (error) { return failure("knowledge_skills_failed", error); } } } export class KnowledgeIngestion extends WorkflowEntrypoint { - async run(event, step) { return runIngestion(this.env, step, event.payload.job_id); } + async run(event, step) { return withDispatchPrincipal(this.env, event.payload.dispatch_principal, () => runIngestion(this.env, step, event.payload.job_id)); } } export default { @@ -98,6 +100,8 @@ export default { || url.search || url.hash || url.username || url.password) fail("not_found", "Unknown Knowledge route.", 404); const body = await readJson(request, SYNC_REQUEST_BYTES); if (body.operation !== "skills.sync" && new TextEncoder().encode(JSON.stringify(body)).length > 65536) fail("request_too_large", "Request is too large.", 413); + const source = bindingPrincipal(request); + if (source.keyId !== body.principal?.id) fail("invalid_principal", "The dispatch principal does not match.", 403); let decision; const result = await dispatchKnowledge(env, body, { signal: request.signal, waitUntil: promise => ctx.waitUntil(promise), onSecretScan: value => { decision = value; } }); const response = reply(result); @@ -108,6 +112,6 @@ export default { async scheduled(_event, env, ctx) { ctx.waitUntil(maintainKnowledge(env).catch(error => { logFailure("knowledge_maintenance_failed", error); throw error; })); // Separate so an upstream check failure cannot stop catalogue maintenance. - ctx.waitUntil(watchImportedSkills(env).catch(error => logFailure("knowledge_skills_watch_failed", error))); + ctx.waitUntil(withDispatchPrincipal(env, { authenticated: true, keyId: env.ADMIN_USERNAME || "admin" }, () => watchImportedSkills(env)).catch(error => logFailure("knowledge_skills_watch_failed", error))); }, }; diff --git a/worker/knowledge/ingestion.mjs b/worker/knowledge/ingestion.mjs index 259d1081..e20d80fb 100644 --- a/worker/knowledge/ingestion.mjs +++ b/worker/knowledge/ingestion.mjs @@ -1,3 +1,4 @@ +import { dispatchContext, withDispatchPrincipal } from "../provider-dispatch.mjs"; import { publicUrl } from "./contracts.mjs"; import { createArtifact, normalizeSourceText } from "./evidence.mjs"; import { KnowledgeCorpus } from "./corpus.mjs"; @@ -24,7 +25,9 @@ const REFUSED_ERRORS = new Set(["provider_disabled", "allowance_exhausted", "led // false "Worker's code had hung" cancellation (cloudflare/workers-sdk#14959). Results cached // as objects by instances started before this change are still accepted. async function durableStep(step, name, config, callback) { - const value = await step.do(name, config, async () => JSON.stringify(await callback())); + const state = dispatchContext(); + const value = await step.do(name, config, () => withDispatchPrincipal(state?.env, state?.authority, + async () => JSON.stringify(await callback()))); return typeof value === "string" ? JSON.parse(value) : value; } diff --git a/worker/knowledge/memos.mjs b/worker/knowledge/memos.mjs index 3aeb6797..9ab765ae 100644 --- a/worker/knowledge/memos.mjs +++ b/worker/knowledge/memos.mjs @@ -1,3 +1,4 @@ +import { providerOperation } from "../provider-dispatch.mjs"; import { fail } from "./contracts.mjs"; import { resolveRetentionPolicy, retentionAllowsContent } from "../retention-policy.mjs"; import { digest } from "./evidence.mjs"; @@ -65,7 +66,7 @@ class MemoSession { usage.push(entry); try { const result = await bounded(() => options.embed ? options.embed(request.query) - : env.KNOWLEDGE_SEARCH_AI.run("@cf/baai/bge-m3", { text: [request.query] }), this.deadline(MEMO_TIMEOUT_MS)); + : providerOperation("cloudflare", () => env.KNOWLEDGE_SEARCH_AI.run("@cf/baai/bge-m3", { text: [request.query] })), this.deadline(MEMO_TIMEOUT_MS)); const vector = Array.isArray(result) ? result : result?.data?.[0]; const quantized = quantizeEmbedding(vector); if (quantized) entry.outcome = "completed"; diff --git a/worker/knowledge/providers/transport.mjs b/worker/knowledge/providers/transport.mjs index 36a223b4..4efa73a6 100644 --- a/worker/knowledge/providers/transport.mjs +++ b/worker/knowledge/providers/transport.mjs @@ -1,3 +1,4 @@ +import { DispatchError, providerFetch, transportOptions } from "../../provider-dispatch.mjs"; import { invalidResponse, ProviderError, upstreamError } from "./errors.mjs"; import { withProviderKey } from "./keys.mjs"; @@ -69,7 +70,7 @@ export async function fetchBounded(provider, url, options, { fetchImpl = fetch, try { if (controller.signal.aborted) throw new Error("aborted"); // Workers supports manual redirects; the non-2xx check rejects them before any follow-up. - const response = await fetchImpl(url, { ...options, redirect: "manual", signal: controller.signal }); + const response = await providerFetch(provider, url, { ...options, signal: controller.signal }, { send: async checked => fetchImpl(checked.url, await transportOptions(checked, options)) }); if (response.status < 200 || response.status >= 300) { let body = null; if ([402, 429].includes(response.status) || (provider === "alexandria" && response.status === 403)) { @@ -83,7 +84,7 @@ export async function fetchBounded(provider, url, options, { fetchImpl = fetch, const text = await readBounded(response, provider, maxResponseBytes); return parseResponse(text, response.headers.get("content-type") || "", provider, acceptText); } catch (error) { - if (error instanceof ProviderError) throw error; + if (error instanceof ProviderError || error instanceof DispatchError) throw error; if (controller.signal.aborted) { throw new ProviderError(provider, "provider_timeout", "The knowledge provider request was cancelled or timed out.", 504); } diff --git a/worker/knowledge/service.mjs b/worker/knowledge/service.mjs index 37d6388d..c827fab7 100644 --- a/worker/knowledge/service.mjs +++ b/worker/knowledge/service.mjs @@ -1,3 +1,4 @@ +import { dispatchContext, withDispatchPrincipal } from "../provider-dispatch.mjs"; import { authorize, fail, fields, parseQuery, publicUrl, validId } from "./contracts.mjs"; import { getAuthority } from "./authority-client.mjs"; import { KnowledgeCorpus } from "./corpus.mjs"; @@ -61,10 +62,10 @@ export async function scheduleSource(env, authority, sourceId, signal, artifactI return allocation.enabled && allocation.background_limit >= allocation.units_per_call; })) fail("background_disabled", "Enable source and index background allowances before scheduling ingestion.", 409); active(); - const job = await authority.call("job.enqueue", { source_id: sourceId, ...(artifactId ? { artifact_id: artifactId } : {}) }); + const job = await authority.call("job.enqueue", { source_id: sourceId, dispatch_principal: dispatchContext()?.authority, ...(artifactId ? { artifact_id: artifactId } : {}) }); try { active(); - const created = await env.KNOWLEDGE_INGESTION.createBatch([{ id: job.id, params: { job_id: job.id } }]); + const created = await env.KNOWLEDGE_INGESTION.createBatch([{ id: job.id, params: { job_id: job.id, dispatch_principal: job.dispatch_principal } }]); if (!created.length) { active(); const instance = await env.KNOWLEDGE_INGESTION.get(job.id); @@ -101,7 +102,7 @@ async function artifactResult(env, authority, id, corpus) { return { artifact, text }; } -export async function dispatchKnowledge(env, envelope, options = {}) { +async function runKnowledge(env, envelope, options = {}) { fields(envelope, ["version", "operation", "principal", "payload", "secret_scan_mode", "secret_scan_checked", "retention_policy"], ["version", "operation", "principal", "payload"]); if (envelope.version !== 1 || !OPERATIONS.has(envelope.operation)) fail("unknown_operation", "Unknown Knowledge operation.", 404); const { operation, principal, payload } = envelope; @@ -145,7 +146,7 @@ export async function dispatchKnowledge(env, envelope, options = {}) { return memos.call(operation === "memos.stats" ? "stats" : "purge", parsed); } if (operation === "policy.update") return { policy: await authority.call("policy.update", payload) }; - if (operation === "sources.create") return { source: await authority.call("source.create", payload), job: null }; + if (operation === "sources.create") return { source: await authority.call("source.create", { ...payload, dispatch_principal: dispatchContext()?.authority }), job: null }; if (operation === "sources.update") return { source: await authority.call("source.update", payload) }; fields(payload, ["id"], ["id"]); if (operation === "sources.refresh") return { job: await scheduleSource(env, authority, payload.id) }; @@ -165,7 +166,10 @@ export async function maintainKnowledge(env, { authority = getAuthority(env), co for (const artifact of await authority.call("artifacts.expired")) { try { const claimed = await authority.call("artifact.expiration_claim", { id: artifact.id, expires_at: artifact.expires_at }); - await corpus.removeArtifact(claimed); + // Fixed storage cleanup uses the current durable operator account; ingestion + // and reconciliation Workflows continue to carry the original source account. + await withDispatchPrincipal(env, { authenticated: true, keyId: env.ADMIN_USERNAME || "admin" }, + () => corpus.removeArtifact(claimed)); await authority.call("artifact.expire", { id: artifact.id, expires_at: artifact.expires_at }); } catch (error) { // The claimed revision stays unreadable until a later run removes it. @@ -180,3 +184,7 @@ export async function maintainKnowledge(env, { authority = getAuthority(env), co .catch(error => logFailure("knowledge_reconcile_not_scheduled", error, { job_id: job.id })); } } + +export async function dispatchKnowledge(env, envelope, options = {}) { + return withDispatchPrincipal(env, { authenticated: true, keyId: envelope?.principal?.id }, () => runKnowledge(env, envelope, options)); +} diff --git a/worker/knowledge/skills-import.mjs b/worker/knowledge/skills-import.mjs index 2af33705..1b5de7de 100644 --- a/worker/knowledge/skills-import.mjs +++ b/worker/knowledge/skills-import.mjs @@ -1,3 +1,4 @@ +import { providerFetch, transportOptions, withDispatchPrincipal } from "../provider-dispatch.mjs"; import { fail, fields, integer } from "./contracts.mjs"; import { digest } from "./evidence.mjs"; import { CLAWHUB, clawhubFile, directory, frontmatter, REF, repository, REVIEW_PATTERNS, reviewFlags } from "./skills-market.mjs"; @@ -62,7 +63,7 @@ async function get(url, headers, options, maxBytes) { const timer = setTimeout(cancel, options.importTimeoutMs ?? TIMEOUT_MS); try { let response; - try { response = await (options.fetch ?? fetch)(url, { headers, redirect: "manual", signal: controller.signal }); } + try { response = await providerFetch("skills", url, { headers, signal: controller.signal }, { send: async checked => (options.fetch ?? fetch)(checked.url, await transportOptions(checked, { headers })) }); } catch { fail(controller.signal.aborted ? "upstream_timeout" : "upstream_unavailable", "The skill source could not be reached.", controller.signal.aborted ? 504 : 502); } if (!response.ok) { await response.body?.cancel(); @@ -338,7 +339,7 @@ export async function checkImports(env, store, options = {}, { force = false, li const due = await store.call("imports.due", { limit, force }); for (const item of due) { let result; - try { result = await checkOne(env, item, options); } + try { result = await withDispatchPrincipal(env, item.origin?.dispatch_principal, () => checkOne(env, item, options)); } catch (error) { result = { status: "error", error: error?.code ?? "upstream_unavailable" }; } await store.call("imports.checked", { skill_id: item.skill_id, pin: pinOf(item.origin), result }); } diff --git a/worker/knowledge/skills-store.mjs b/worker/knowledge/skills-store.mjs index 701157c7..5dfaad33 100644 --- a/worker/knowledge/skills-store.mjs +++ b/worker/knowledge/skills-store.mjs @@ -1,3 +1,4 @@ +import { dispatchContext, providerOperation } from "../provider-dispatch.mjs"; import { fail, fields, isRecord } from "./contracts.mjs"; import { digest } from "./evidence.mjs"; import { parseFind, parseGet, parseSync, validateSkill, skillKey, SKILLS_LIMIT } from "./skills-validation.mjs"; @@ -22,7 +23,7 @@ async function bounded(callback, milliseconds) { export class SkillsStore { constructor(storage, env, { embed, clock = Date.now } = {}) { Object.assign(this, { storage, sql: storage.sql, env, clock }); - this.embed = embed ?? (text => env.KNOWLEDGE_SEARCH_AI?.run("@cf/baai/bge-m3", { text: [text] })); + this.embed = embed ?? (text => providerOperation("cloudflare", () => env.KNOWLEDGE_SEARCH_AI?.run("@cf/baai/bge-m3", { text: [text] }))); this.index = null; this.syncTail = Promise.resolve(); this.pendingR2 = new Map(); @@ -135,7 +136,7 @@ export class SkillsStore { const { record, files } = validated, old = this.read(record.skill_id); if (old && old.root !== record.root) return { skill_id: record.skill_id, status: "rejected", reason: "root_conflict" }; const status = !old ? "created" : old.content_hash === record.content_hash ? "unchanged" : "updated"; - if (status === "unchanged" && origin && !dryRun && pinOf(old.origin) !== pinOf(origin)) { + if (status === "unchanged" && origin && !dryRun && (pinOf(old.origin) !== pinOf(origin) || !old.origin?.dispatch_principal)) { // Identical content at a new pin moves only the pin. this.storage.transactionSync(() => { const row = this.rows("SELECT record FROM skills WHERE skill_id = ?", record.skill_id)[0]; @@ -213,7 +214,9 @@ export class SkillsStore { return { skill_id: skill.skill_id, status: "rejected", reason: "skills_limit" }; } const result = await this.syncSkill(skill, payload.dry_run === true, performance.now() + 45000, - { ...origin, imported_at: new Date(this.clock()).toISOString() }); + { ...origin, dispatch_principal: existing?.origin?.dispatch_principal ?? (dispatchContext()?.authority?.authenticated === true + ? { authenticated: true, keyId: dispatchContext().authority.keyId } : null), + imported_at: new Date(this.clock()).toISOString() }); if (!payload.dry_run && result.status !== "rejected") this.ledger.forget(skill.skill_id); return result; } diff --git a/worker/knowledge/skills.mjs b/worker/knowledge/skills.mjs index 8ed63eff..64160011 100644 --- a/worker/knowledge/skills.mjs +++ b/worker/knowledge/skills.mjs @@ -1,3 +1,4 @@ +import { dispatchContext } from "../provider-dispatch.mjs"; import { fail } from "./contracts.mjs"; import { checkImports, importSkill, reportSkills } from "./skills-import.mjs"; import { discoverSkills, previewSkill } from "./skills-market.mjs"; @@ -8,7 +9,7 @@ function skillsStore(env, options) { async call(operation, payload, principal) { const stub = env.KNOWLEDGE_SKILLS.get(env.KNOWLEDGE_SKILLS.idFromName("personal")); const response = await stub.fetch("http://skills.internal/dispatch", { method: "POST", - headers: { "content-type": "application/json" }, body: JSON.stringify({ operation, payload, principal }), signal: AbortSignal.timeout(50000) }); + headers: { "content-type": "application/json", "X-MultiLLM-Dispatch-Principal": JSON.stringify({ version: 1, username: dispatchContext()?.authority?.keyId }) }, body: JSON.stringify({ operation, payload, principal }), signal: AbortSignal.timeout(50000) }); const body = await response.json(); if (!response.ok || body.version !== 1) fail(body.error?.code ?? "skills_unavailable", body.error?.message ?? "Skills storage is unavailable.", response.ok ? 503 : response.status); return body.result; diff --git a/worker/media-outbound.mjs b/worker/media-outbound.mjs index 758779a1..db7d4348 100644 --- a/worker/media-outbound.mjs +++ b/worker/media-outbound.mjs @@ -1,3 +1,4 @@ +import { bindingPrincipal, withDispatchPrincipal, providerFetch, dispatchFailure, DispatchError } from "./provider-dispatch.mjs"; /** * Media I/O for the Container, reachable only through its private outbound handler * (`http://media.internal`). The Worker fetches caller-supplied image URLs, so a URL can @@ -77,8 +78,7 @@ export async function boundedBytes(response, maxBytes) { export async function fetchPublic(value, { accept = "image/*", fetcher = fetch } = {}) { let url = publicHttpsUrl(value); for (let hop = 0; url && hop <= MAX_REDIRECTS; hop += 1) { - const response = await fetcher(url.href, { redirect: "manual", headers: { accept }, - signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + const response = await providerFetch("media", url.href, { headers: { accept }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }, { publicAsset: true, send: checked => fetcher(checked.url, { headers: checked.headers, redirect: checked.redirect, signal: checked.signal }) }); if (!REDIRECT_STATUSES.has(response.status)) return response; const location = response.headers.get("location"); await response.body?.cancel(); @@ -227,7 +227,7 @@ async function fileOperation(request, env, id, suffix) { return mediaFailure("not_found", "Unknown media operation.", 404); } -export async function handleMediaOutbound(request, env) { +async function runMediaOutbound(request, env) { const url = new URL(request.url); if (url.origin !== ORIGIN || url.search || url.hash || url.username || url.password) { return mediaFailure("not_found", "Unknown media operation.", 404); @@ -238,7 +238,18 @@ export async function handleMediaOutbound(request, env) { if (file) return await fileOperation(request, env, file[1], file[2]); return mediaFailure("not_found", "Unknown media operation.", 404); } catch (error) { + if (error instanceof DispatchError) return dispatchFailure(error); logFailure("media_outbound_failed", error, { operation: url.pathname.split("/")[2] ?? "unknown" }); return mediaFailure("media_unavailable", "The media operation failed.", 502); } } + +export async function handleMediaOutbound(request, env) { + if (new URL(request.url).search || new URL(request.url).origin !== ORIGIN) return new Response("Not found", { status: 404 }); + if (request.method !== "POST" && new URL(request.url).pathname === "/v1/fetch") return new Response("Not found", { status: 404 }); + // R2 management is independent of provider dispatch. Only external I/O needs an identity. + const path = new URL(request.url).pathname; + if (path !== "/v1/fetch" && !path.endsWith("/import")) return runMediaOutbound(request, env); + try { return await withDispatchPrincipal(env, bindingPrincipal(request), () => runMediaOutbound(request, env)); } + catch (error) { return dispatchFailure(error); } +} diff --git a/worker/native-config-sync.mjs b/worker/native-config-sync.mjs index 5ce8e4dd..9549e1af 100644 --- a/worker/native-config-sync.mjs +++ b/worker/native-config-sync.mjs @@ -28,6 +28,7 @@ async function loadAccounts(db) { after = row.username; // Bootstrap admin keys use environment authority, never these stored key hashes. controls.set(row.username, Object.freeze({ scopes: row.scopes, allowed_models: row.allowed_models, + official_only: row.official_only, forced_provider_preferences: row.forced_provider_preferences, revoked_at: row.revoked_at, expires_at: row.expires_at, allowed_ips: row.allowed_ips })); } if (results.length < 200) return controls; diff --git a/worker/provider-dispatch.mjs b/worker/provider-dispatch.mjs new file mode 100644 index 00000000..3ce76bb6 --- /dev/null +++ b/worker/provider-dispatch.mjs @@ -0,0 +1,125 @@ +/** Sole provider network boundary. Deferred work carries identity, never cached policy. */ +import { AsyncLocalStorage } from "node:async_hooks"; +import { currentDataControls, mergePreferences, parsePreferences, providerClassPermitted } from "./key-data-guards.mjs"; +import { admittedNetwork } from "./dispatch-network.mjs"; +import { dataPolicy } from "./dispatch-policy.mjs"; + +const context = new AsyncLocalStorage(); +export const dispatchContext = () => context.getStore(); +export const withDispatchPrincipal = (env, authority, run) => context.run({ env, authority }, run); +export class DispatchError extends Error { + constructor(code, status = 503) { super(code); this.code = code; this.status = status; } +} +export const dispatchFailure = error => Response.json({ error: { code: error.code ?? "key_data_controls_unavailable" } }, + { status: error.status ?? 503 }); + +export function bindingPrincipal(request) { + try { + const value = JSON.parse(request.headers.get("X-MultiLLM-Dispatch-Principal")); + if (value.version !== 1 || typeof value.username !== "string" || !value.username + || Object.keys(value).sort().join() !== "username,version") throw Error(); + return { authenticated: true, keyId: value.username }; + } catch { throw new DispatchError("key_data_controls_unavailable"); } +} + +async function admission(provider, options) { + const state = options.env ? options : dispatchContext(); + try { + if (!state?.authority || state.authority.authenticated !== true) throw Error(); + const user = await currentDataControls(state.env, state.authority); + const policy = dataPolicy(user); + if (!providerClassPermitted(policy, provider, state.env)) throw new DispatchError("provider_class_not_allowed", 403); + return { ...state, policy }; + } catch (error) { + if (error instanceof DispatchError) throw error; + throw new DispatchError("key_data_controls_unavailable"); + } +} + +const apiOrigins = { + openai: "https://api.openai.com", anthropic: "https://api.anthropic.com", xai: "https://api.x.ai", + gemini: "https://generativelanguage.googleapis.com", gemma: "https://generativelanguage.googleapis.com", + openrouter: "https://openrouter.ai", cerebras: "https://api.cerebras.ai", groq: "https://api.groq.com", + together: "https://api.together.xyz", azure: "https://models.inference.ai.azure.com", + hyperbolic: "https://api.hyperbolic.xyz", sambanova: "https://api.sambanova.ai", scaleway: "https://api.scaleway.ai", + opencode: "https://opencode.ai", nanogpt: "https://nano-gpt.com", linkapi: "https://hk.linkapi.ai", +}; +function apiOrigin(provider, url, env, options) { + const prefix = provider.toUpperCase().replaceAll("-", "_"); + const configured = env[`${prefix}_BASE_URL`] ?? (provider === "opencode" ? env.OPENCODE_GO_BASE_URL : null); + try { + const allowed = [apiOrigins[provider], configured].filter(Boolean).map(value => new URL(value).origin); + if (options.realtime) { + for (const [model, entry] of Object.entries(JSON.parse(env.REALTIME_PROVIDERS_JSON ?? "{}"))) { + if (!model.startsWith(provider + ":")) continue; + const endpoint = new URL(entry.url); + if (endpoint.protocol !== "wss:" || endpoint.username || endpoint.password) continue; + endpoint.protocol = "https:"; + allowed.push(endpoint.origin); + } + } + return url.protocol === "https:" && !url.username && !url.password && allowed.includes(url.origin); + } catch { return false; } +} + +function credentials(provider, request, env, policy, options) { + if (!policy.official_only && policy.forced_provider_preferences === null || provider === "cloudflare") return request; + const url = new URL(request.url); + if ([...url.searchParams.keys()].some(name => name.toLowerCase() === "key")) { + throw new DispatchError("provider_credential_not_allowed", 403); + } + if (!apiOrigin(provider, url, env, options)) throw new DispatchError("attested_credential_unavailable"); + const names = { gemini: ["GEMINI_API_KEY"], opencode: ["OPENCODE_GO_API_KEY", "OPENCODE_API_KEY"] }[provider] + ?? [`${provider.toUpperCase().replaceAll("-", "_")}_API_KEY`]; + const key = names.map(name => env[name]).find(value => typeof value === "string" && value.trim() + && !/^(?:your[-_]|changeme|replace[-_]?me|<)/i.test(value)); + if (!key) throw new DispatchError("attested_credential_unavailable"); + const headers = new Headers(request.headers); + for (const name of ["authorization", "x-api-key", "x-goog-api-key", "api-key"]) headers.delete(name); + const name = { gemini: "x-goog-api-key", anthropic: "x-api-key", azure: "api-key" }[provider] ?? "authorization"; + headers.set(name, name === "authorization" ? `Bearer ${key}` : key); + return new Request(request, { headers }); +} + +export async function providerFetch(provider, input, init = {}, options = {}) { + const state = await admission(provider, options); + const forced = parsePreferences(state.policy.forced_provider_preferences); + if ((options.realtime || options.unsupportedPreferences) && forced !== null) throw new DispatchError("forced_provider_preferences_not_supported", 403); + let request = new Request(input, init); + if (options.publicAsset) { + const headers = new Headers(request.headers); + for (const name of ["authorization", "proxy-authorization", "x-api-key", "x-goog-api-key", "api-key", "cookie"]) headers.delete(name); + request = new Request(request, { headers }); + } else request = credentials(provider, request, state.env, state.policy, options); + if (provider === "openrouter" && forced !== null && request.body) { + let payload; + try { payload = await request.clone().json(); if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw Error(); } + catch { throw new DispatchError("invalid_request", 400); } + payload.provider = mergePreferences(payload.provider, forced); + const headers = new Headers(request.headers); headers.delete("content-length"); + request = new Request(request, { headers, body: JSON.stringify(payload) }); + } + const headers = new Headers(request.headers); headers.delete("x-multillm-dispatch-principal"); + request = new Request(request, { headers, redirect: "manual" }); + return admittedNetwork(() => (options.send ?? fetch)(request)); +} + +/** SDK/binding calls use the same admission, including embeddings and background work. */ +export async function providerOperation(provider, run, options = {}) { + const state = await admission(provider, options); + if (provider === "openrouter" && state.policy.forced_provider_preferences !== null) { + throw new DispatchError("forced_provider_preferences_not_supported", 403); + } + return admittedNetwork(run); +} + +/** Keep adapter transports compatible while passing only the admitted request. */ +export async function transportOptions(request, original = {}) { + const headers = {}; + for (const [name, value] of request.headers) { + const spelling = Object.keys(original.headers ?? {}).find(key => key.toLowerCase() === name) ?? name; + headers[spelling] = value; + } + return { ...original, method: request.method, headers, body: request.body ? await request.text() : undefined, + signal: request.signal, redirect: request.redirect }; +} diff --git a/worker/realtime.mjs b/worker/realtime.mjs index e1fa50ba..e6da2e79 100644 --- a/worker/realtime.mjs +++ b/worker/realtime.mjs @@ -1,4 +1,6 @@ +import { providerFetch } from "./provider-dispatch.mjs"; /** Native gateway authentication, ticket admission and approved Realtime dispatch. */ +import { currentDataControls, parsePreferences, providerClassPermitted } from "./key-data-guards.mjs"; import { Buffer } from "node:buffer"; import { createHash, scrypt, timingSafeEqual } from "node:crypto"; import { activeUsersByPrefix, boundedBody, keyControlsPermit, validUser } from "./control-users-d1.mjs"; @@ -67,13 +69,23 @@ function modelGranted(patterns, model) { return patterns.split(",").some(pattern => new RegExp("^" + pattern.toLowerCase().split("*") .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$").test(model.toLowerCase())); } -function permit(principal, request, model, now) { +async function permit(principal, request, model, now, env) { if (!principal || principal.revoked_at || !keyControlsPermit(principal, request.headers.get("cf-connecting-ip"), now)) { throw new RealtimeError("invalid_api_key", 401); } if (!principal.scopes.includes("chat") || !principal.scopes.includes("audio") || !modelGranted(principal.allowed_models, model)) { throw new RealtimeError("model_not_granted", 403); } + let controls; + try { controls = await currentDataControls(env, { principal, keyId: principal.owner }); } + catch { throw new RealtimeError("key_data_controls_unavailable", 503); } + if (!providerClassPermitted(controls, model.split(":", 1)[0], env)) { + throw new RealtimeError("provider_class_not_allowed", 403); + } + // Realtime frames cannot enforce HTTP provider preferences throughout a session. + if (parsePreferences(controls.forced_provider_preferences) !== null) { + throw new RealtimeError("forced_provider_preferences_not_supported", 403); + } return principal; } function account(row) { @@ -124,7 +136,7 @@ async function authenticate(request, env, model, now) { if (!principal) throw new RealtimeError("invalid_api_key", 401); await tenantPrincipal(env, principal); principal.principal_hash = await principalHash(`realtime-key:${digest(key)}`); - return permit(principal, request, model, now); + return permit(principal, request, model, now, env); } async function signingKey(env) { // Derive a domain-specific key from the existing gateway signing binding. @@ -171,7 +183,7 @@ async function consumeTicket(ticket, request, env, model, now) { if (credential) principal = integration(await lookupIntegrationPrincipal(env.INTELLIGENCE_DB, credential.key_prefix)); } if (!principal || principal.owner !== row.owner || !same(principal.credential_fingerprint, row.credential_fingerprint)) throw new RealtimeError("invalid_api_key", 401); - permit(principal, request, model, now); + await permit(principal, request, model, now, env); const consumed = await env.INTELLIGENCE_DB.prepare("UPDATE realtime_tickets SET consumed_at=? WHERE nonce=? AND consumed_at IS NULL AND expires_at>?") .bind(now, match[2], now).run(); if (consumed.meta.changes !== 1) throw new RealtimeError("invalid_api_key", 401); @@ -240,8 +252,10 @@ async function openSession(request, env, ctx, settings, options) { if (request.signal.aborted || controller.signal.aborted) throw new RealtimeError("realtime_canceled", 499); const url = new URL(settings.providers.get(model)); url.protocol = "https:"; handedOff = true; - const response = await (options.dial ?? fetch)(url.href, { method: "GET", headers: upstreamHeaders(request, key), - redirect: "manual", signal: controller.signal }); + const response = await providerFetch(provider, url.href, { method: "GET", headers: upstreamHeaders(request, key), + signal: controller.signal }, { env, authority: { authenticated: true, keyId: principal.owner }, realtime: true, + send: checked => (options.dial ?? fetch)(checked.url, { method: checked.method, headers: checked.headers, + redirect: checked.redirect, signal: checked.signal }) }); socket = response.webSocket; if (response.status !== 101 || !socket) { void response.body?.cancel?.().catch(() => {}); throw new RealtimeError("realtime_upstream_error", 502); } if (request.signal.aborted || controller.signal.aborted) throw new RealtimeError("realtime_canceled", 499); diff --git a/worker/request-telemetry.mjs b/worker/request-telemetry.mjs index 11a2f1ac..7d136de6 100644 --- a/worker/request-telemetry.mjs +++ b/worker/request-telemetry.mjs @@ -11,7 +11,7 @@ const millis = value => Math.min(86_400_000, Math.max(0, Math.round(value))); export function createUsageObserver(sse = false, onToken = () => {}) { let carry = new Uint8Array(), data = [], dataBytes = 0, oversized = false, eventOversized = false; - let usage = { input_tokens: null, output_tokens: null }, completed = false, failed = false; + let usage = { input_tokens: null, output_tokens: null, cached_input_tokens: null, reasoning_tokens: null }, completed = false, failed = false; const consume = text => { if (text.trim() === "[DONE]") { completed = true; return; } let value; @@ -19,7 +19,14 @@ export function createUsageObserver(sse = false, onToken = () => {}) { if (!value || typeof value !== "object") return; const observed = value.usage ?? value.response?.usage ?? value.message?.usage; if (observed && typeof observed === "object") { - const input = component(observed.prompt_tokens ?? observed.input_tokens); + let input = component(observed.prompt_tokens ?? observed.input_tokens); + const cached = component(observed.prompt_tokens_details?.cached_tokens ?? observed.input_tokens_details?.cached_tokens ?? observed.cache_read_input_tokens); + const reasoning = component(observed.completion_tokens_details?.reasoning_tokens ?? observed.output_tokens_details?.reasoning_tokens); + if (cached !== null) usage.cached_input_tokens = cached; + if (reasoning !== null) usage.reasoning_tokens = reasoning; + if (input !== null && (Object.hasOwn(observed, "cache_read_input_tokens") || Object.hasOwn(observed, "cache_creation_input_tokens"))) { + input += (cached ?? 0) + (component(observed.cache_creation_input_tokens) ?? 0); + } const output = component(observed.completion_tokens ?? observed.output_tokens); if (input !== null) usage.input_tokens = input; if (output !== null) usage.output_tokens = output; diff --git a/worker/roleplay/credential-health.mjs b/worker/roleplay/credential-health.mjs index 960dfbff..a244bd38 100644 --- a/worker/roleplay/credential-health.mjs +++ b/worker/roleplay/credential-health.mjs @@ -1,3 +1,4 @@ +import { providerFetch } from "../provider-dispatch.mjs"; import { buildProviderHeaders } from "./config.mjs"; const DEFINITE_KEY_FAILURES = new Set([401, 402, 403, 429]); @@ -52,12 +53,13 @@ async function probeCredential(candidate, env, settings, signal) { settings.nanogptKeyCheckTimeoutMs, ); try { - return await fetch(candidate.catalogEndpoint, { + return await providerFetch(candidate.provider, candidate.catalogEndpoint, { method: "GET", headers: buildProviderHeaders(candidate, env), redirect: "manual", signal: controller.signal, - }); + }, { env, unsupportedPreferences: true, authority: { authenticated: true, keyId: env.ADMIN_USERNAME || "admin" }, + send: checked => fetch(checked.url, { method: checked.method, headers: checked.headers, redirect: checked.redirect, signal: checked.signal }) }); } finally { clearTimeout(timeout); signal?.removeEventListener("abort", forwardAbort); diff --git a/worker/roleplay/transport.mjs b/worker/roleplay/transport.mjs index 32a53389..5d0b957e 100644 --- a/worker/roleplay/transport.mjs +++ b/worker/roleplay/transport.mjs @@ -1,3 +1,4 @@ +import { providerFetch, dispatchFailure, DispatchError } from "../provider-dispatch.mjs"; import { UpstreamCancellation, readBoundedUpstreamBytes } from "../upstream-cancellation.mjs"; import { firewallFetch, isSecretScanBlock } from "../secret-firewall.mjs"; import { piiFetch, isPIIFailure } from "../pii-rehydration.mjs"; @@ -581,21 +582,15 @@ async function fetchCandidate(candidate, payload, env, settings, signal, key) { }; cancellation.handoff(); const piiOptions = { route: "/v1/roleplay/chat/completions", keyScope: settings.piiKeyScope ?? "", onDecision: settings.onPIIDecision }; - const submit = checked => piiFetch(checked, env, piiOptions, async transformed => - useOpenCodeContainer ? containerNamespace.getByName("primary").fetch(transformed) - : fetch(transformed.url, { ...requestInit, headers: transformed.headers, body: await transformed.text() })); - const response = useOpenCodeContainer && !resolvePIIPolicy(env, piiOptions) - ? await containerNamespace.getByName("primary").fetch( - new Request( - "https://roleplay.internal/opencode/v1/chat/completions", - requestInit, - ), - ) - : await firewallFetch(new Request(useOpenCodeContainer ? "https://roleplay.internal/opencode/v1/chat/completions" : candidate.endpoint, requestInit), env, { - route: "/v1/roleplay/chat/completions", provider: candidate.provider, - onDecision: settings.onSecretScan, - principal: { id: env.ADMIN_USERNAME || "admin" }, - }, submit); + const authority = { authenticated: true, keyId: env.ADMIN_USERNAME || "admin" }; + const submit = checked => piiFetch(checked, env, piiOptions, transformed => providerFetch(candidate.provider, transformed, {}, { + env, authority, unsupportedPreferences: true, + send: async permitted => useOpenCodeContainer ? containerNamespace.getByName("primary").fetch(permitted) : fetch(permitted.url, { ...requestInit, headers: permitted.headers, body: await permitted.text() }), + }).catch(error => { if (!(error instanceof DispatchError)) throw error; return dispatchFailure(error); })); + const response = await firewallFetch(new Request(useOpenCodeContainer ? "https://roleplay.internal/opencode/v1/chat/completions" : candidate.endpoint, requestInit), env, { + route: "/v1/roleplay/chat/completions", provider: candidate.provider, + onDecision: settings.onSecretScan, principal: { id: authority.keyId }, + }, submit); cancellation.headersReceived(); const ownedResponse = isSecretScanBlock(response) || isPIIFailure(response) ? response : cancellation.wrapResponse(response); diff --git a/worker/scheduled-maintenance.mjs b/worker/scheduled-maintenance.mjs index 175d264f..b4636ada 100644 --- a/worker/scheduled-maintenance.mjs +++ b/worker/scheduled-maintenance.mjs @@ -1,3 +1,4 @@ +import { controlNetwork } from "./dispatch-network.mjs"; /** Bounded shared-cache maintenance and content-free alert delivery. */ import { cleanupGenerationCache } from "./generation-cache-d1.mjs"; import { generationCacheSettings } from "./exact-generation-cache.mjs"; @@ -34,8 +35,8 @@ export async function collectAlertAggregates(env, rules, now = new Date()) { export function alertTransport(destination, options) { // The delivery owner provides its three-second abort signal and bounds the payload. - return fetch(destination, { method: "POST", headers: options.headers, body: options.body, - redirect: "error", signal: options.signal }); + return controlNetwork(destination, () => fetch(destination, { method: "POST", headers: options.headers, body: options.body, + redirect: "error", signal: options.signal })); } /** Scheduled work is registered only when a feature that needs it is enabled. */ diff --git a/worker/secret-firewall.mjs b/worker/secret-firewall.mjs index c46ce09c..37b502be 100644 --- a/worker/secret-firewall.mjs +++ b/worker/secret-firewall.mjs @@ -1,3 +1,4 @@ +import { providerFetch } from "./provider-dispatch.mjs"; /** Provider dispatch policy. Audit failures never change the upstream request. */ import { redactPayload, scanText } from "./secret-scan.mjs"; @@ -124,8 +125,9 @@ function multipart(bytes, contentType) { }]; } -export async function firewallFetch(request, env, options = {}, fetchImpl = fetch) { - if (!request.body || secretScanMode(env, options.principal) === "off") return fetchImpl(request); +export async function firewallFetch(request, env, options = {}, fetchImpl) { + const send = fetchImpl ?? (checked => providerFetch(options.provider, checked, {}, { env, authority: options })); + if (!request.body || secretScanMode(env, options.principal) === "off") return send(request); let protectedRequest = request, decision; try { const bytes = await boundedBody(request); @@ -159,7 +161,7 @@ export async function firewallFetch(request, env, options = {}, fetchImpl = fetc const reason = error?.message; console.warn(reason === "scan_timeout" || reason === "scan_body_limit" ? reason : "secret_scan_body_unavailable"); } - const upstream = await fetchImpl(protectedRequest); + const upstream = await send(protectedRequest); if (!decision?.header || !upstream.ok) return upstream; const response = new Response(upstream.body, upstream); response.headers.set(SECRET_SCAN_HEADER, decision.header); diff --git a/worker/usage-buckets-d1.mjs b/worker/usage-buckets-d1.mjs index ea81b620..bf2294d9 100644 --- a/worker/usage-buckets-d1.mjs +++ b/worker/usage-buckets-d1.mjs @@ -5,7 +5,7 @@ export const BUCKET_FIELDS = Object.freeze(["ordinary_input_tokens", "cache_read "ordinary_input_cost_microusd", "cache_read_input_cost_microusd", "cache_write_input_cost_microusd", "output_cost_microusd", "bucket_basis", "bucket_source"]); const BASE_FIELDS = Object.freeze(["at", "principal", "key_prefix", "kind", "endpoint", "requested_model", "selected_model", - "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id"]); + "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id", "cached_input_tokens", "reasoning_tokens"]); const KINDS = new Set(["chat", "responses", "images", "videos", "embeddings", "audio", "proxy"]); const MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,255}$/; const ENDPOINT = /^\/[A-Za-z0-9._~:/@+-]{0,255}$/; @@ -19,12 +19,14 @@ const count = (value, maximum = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger const timestamp = value => typeof value === "string" && AT.test(value) && Number.isFinite(Date.parse(value)); export function validBaseUsageRow(row) { + if (row) row = { cached_input_tokens: null, reasoning_tokens: null, ...row }; return row !== null && typeof row === "object" && !Array.isArray(row) && fields(row, BASE_FIELDS) && timestamp(row.at) && text(row.principal, 256) && optional(row.key_prefix, value => text(value, 64)) && KINDS.has(row.kind) && typeof row.endpoint === "string" && ENDPOINT.test(row.endpoint) && optional(row.requested_model, value => typeof value === "string" && MODEL.test(value)) && optional(row.selected_model, value => typeof value === "string" && MODEL.test(value)) && Number.isSafeInteger(row.status) && row.status >= 100 && row.status <= 599 + && optional(row.cached_input_tokens, count) && optional(row.reasoning_tokens, count) && count(row.latency_ms, 86_400_000) && optional(row.input_tokens, count) && optional(row.output_tokens, count) && optional(row.cost_usd, value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1_000_000) && (row.cost_basis === null || row.cost_basis === "usage" || row.cost_basis === "estimate") @@ -51,7 +53,7 @@ export function usageBucketsEnabled(env) { export function serializeUsageBuckets(row, env) { if (!row || typeof row !== "object" || Array.isArray(row)) throw new TypeError("invalid usage row"); - const base = Object.fromEntries(BASE_FIELDS.map(name => [name, row[name]])); + const base = Object.fromEntries(BASE_FIELDS.map(name => [name, row[name] ?? null])); if (!validBaseUsageRow(base)) throw new TypeError("invalid usage row"); const allowed = new Set([...BASE_FIELDS, ...BUCKET_FIELDS]); if (Object.keys(row).some(name => !allowed.has(name))) throw new TypeError("invalid usage fields"); @@ -91,7 +93,7 @@ export async function recordUsageWithBuckets(env, body, { recordBase, recordExte } catch { return Response.json({ version: 1, error: { code: "invalid_request", message: "Usage ledger operation failed" } }, { status: 400 }); } - const legacy = { ...body, rows: rows.map(row => Object.fromEntries(BASE_FIELDS.map(name => [name, row[name]]))) }; + const legacy = { ...body, rows: rows.map(row => Object.fromEntries(BASE_FIELDS.map(name => [name, row[name] ?? null]))) }; const headers = { "cache-control": "no-store" }; if (!enabled) return Response.json(await recordBase(legacy), { headers }); try { diff --git a/worker/usage-ledger-d1.mjs b/worker/usage-ledger-d1.mjs index bb3692d6..8e011470 100644 --- a/worker/usage-ledger-d1.mjs +++ b/worker/usage-ledger-d1.mjs @@ -16,7 +16,7 @@ export const LATENCY_BUCKETS = Object.freeze([250, 500, 1000, 2000, 4000, 8000, const BUCKET_COLUMNS = [...LATENCY_BUCKETS.map(bound => `lat_le_${bound}`), "lat_gt_120000"]; const GROUPS = Object.freeze({ day: "day", model: "model", principal: "principal" }); const EVENT_COLUMNS = ["id", "at", "principal", "key_prefix", "kind", "endpoint", "requested_model", "selected_model", - "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id"]; + "status", "latency_ms", "input_tokens", "output_tokens", "cost_usd", "cost_basis", "request_id", "cached_input_tokens", "reasoning_tokens"]; const ROW_FIELDS = EVENT_COLUMNS.slice(1); const MAX_BODY_BYTES = 262144; const MAX_ROWS = 500; @@ -49,14 +49,15 @@ const APPLIED = "EXISTS (SELECT 1 FROM usage_batches WHERE id = ?2 AND token = ? const INSERT_EVENTS = `INSERT INTO usage_events (at, day, ${ROW_FIELDS.slice(1).join(", ")}) SELECT ${field("at")}, substr(${field("at")}, 1, 10), ${ROW_FIELDS.slice(1).map(field).join(", ")} FROM json_each(?1) WHERE ${APPLIED}`; -const TOTALS = ["requests", "errors", "input_tokens", "output_tokens", "cost_usd", "priced_requests", "latency_ms_total", ...BUCKET_COLUMNS]; +const TOTALS = ["requests", "errors", "input_tokens", "output_tokens", "cost_usd", "priced_requests", "latency_ms_total", ...BUCKET_COLUMNS, "cached_input_tokens", "reasoning_tokens"]; const UPSERT_DAILY = `INSERT INTO usage_daily (day, principal, model, ${TOTALS.join(", ")}) SELECT substr(${field("at")}, 1, 10) AS usage_day, ${field("principal")} AS usage_principal, COALESCE(${field("selected_model")}, ${field("requested_model")}, 'unknown') AS usage_model, COUNT(*), SUM(${field("status")} >= 400), COALESCE(SUM(${field("input_tokens")}), 0), COALESCE(SUM(${field("output_tokens")}), 0), COALESCE(SUM(${field("cost_usd")}), 0), SUM(${field("cost_usd")} IS NOT NULL), SUM(${field("latency_ms")}), - ${BUCKET_COLUMNS.map((_, index) => `SUM(${field("bucket")} = ${index})`).join(", ")} + ${BUCKET_COLUMNS.map((_, index) => `SUM(${field("bucket")} = ${index})`).join(", ")}, + COALESCE(SUM(${field("cached_input_tokens")}), 0), COALESCE(SUM(${field("reasoning_tokens")}), 0) FROM json_each(?1) WHERE ${APPLIED} GROUP BY usage_day, usage_principal, usage_model ON CONFLICT(day, principal, model) DO UPDATE SET ${TOTALS.map(name => `${name} = ${name} + excluded.${name}`).join(", ")}`; @@ -64,7 +65,7 @@ const SUMS = TOTALS.map(name => `SUM(${name}) AS ${name}`).join(", "); function summaryRow(group, row) { const result = { [group]: row[group] }; - for (const name of TOTALS.slice(0, 7)) result[name] = row[name] ?? 0; + for (const name of [...TOTALS.slice(0, 7), "cached_input_tokens", "reasoning_tokens"]) result[name] = row[name] ?? 0; result.latency_buckets = BUCKET_COLUMNS.map(name => row[name] ?? 0); return result; } @@ -203,7 +204,7 @@ export async function recordNativeUsage(env, event, ctx) { const row = { at: new Date().toISOString(), principal: organisationsEnabled(env) ? tenantStorageKey(event.principal, event.tenantContext) : event.principal, key_prefix: null, kind, endpoint: event.endpoint, requested_model: model, selected_model: model, status: event.status, latency_ms: event.duration_ms, input_tokens: event.input_tokens, - output_tokens: event.output_tokens, cost_usd: event.cost_usd, cost_basis: event.cost_basis, + output_tokens: event.output_tokens, cached_input_tokens: event.cached_input_tokens ?? null, reasoning_tokens: event.reasoning_tokens ?? null, cost_usd: event.cost_usd, cost_basis: event.cost_basis, request_id: event.requestId }; if (!validRow(row)) throw new Error("Invalid native usage metadata"); submitNativeEvent(env, event, ctx); diff --git a/wrangler.jsonc b/wrangler.jsonc index 9807bd6e..deb6925c 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -4,7 +4,7 @@ "keep_vars": true, "main": "./cloudflare-worker.mjs", "build": { - "command": "python scripts/build_release_metadata.py" + "command": "node scripts/key_data_preflight.mjs --deploy && python scripts/build_release_metadata.py" }, "version_metadata": { "binding": "CF_VERSION_METADATA" diff --git a/wrangler.knowledge.jsonc b/wrangler.knowledge.jsonc index c88b2b1a..4183e10c 100644 --- a/wrangler.knowledge.jsonc +++ b/wrangler.knowledge.jsonc @@ -3,7 +3,7 @@ "name": "multillm-knowledge", "main": "worker/knowledge/index.mjs", "compatibility_date": "2026-07-28", - "compatibility_flags": ["enable_request_signal", "request_signal_passthrough"], + "compatibility_flags": ["nodejs_compat", "enable_request_signal", "request_signal_passthrough"], "workers_dev": false, "preview_urls": false, "d1_databases": [{