graph-agents-cli 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_agents_cli/__init__.py +26 -0
- graph_agents_cli/_api_policy.py +2145 -0
- graph_agents_cli/_approvals.py +400 -0
- graph_agents_cli/_build.py +186 -0
- graph_agents_cli/_build_info.json +7 -0
- graph_agents_cli/_chat_client.py +462 -0
- graph_agents_cli/_click.py +157 -0
- graph_agents_cli/_defaults.py +139 -0
- graph_agents_cli/_experiments.py +64 -0
- graph_agents_cli/_http.py +192 -0
- graph_agents_cli/_output.py +83 -0
- graph_agents_cli/_project.py +462 -0
- graph_agents_cli/_remote.py +220 -0
- graph_agents_cli/_response_schema.py +264 -0
- graph_agents_cli/_runner.py +319 -0
- graph_agents_cli/_skills_check.py +274 -0
- graph_agents_cli/_tools.py +189 -0
- graph_agents_cli/_trust.py +66 -0
- graph_agents_cli/api/__init__.py +15 -0
- graph_agents_cli/api/_changes.py +506 -0
- graph_agents_cli/api/_files.py +658 -0
- graph_agents_cli/api/cmd_api.py +2480 -0
- graph_agents_cli/deploy/__init__.py +15 -0
- graph_agents_cli/deploy/_config.py +171 -0
- graph_agents_cli/deploy/_image.py +128 -0
- graph_agents_cli/deploy/_kube.py +286 -0
- graph_agents_cli/deploy/_modes.py +234 -0
- graph_agents_cli/deploy/_preflight.py +370 -0
- graph_agents_cli/deploy/_values.py +168 -0
- graph_agents_cli/deploy/cmd_deploy.py +1866 -0
- graph_agents_cli/deploy/gitops.py +562 -0
- graph_agents_cli/deploy/local_load.py +273 -0
- graph_agents_cli/dev/__init__.py +13 -0
- graph_agents_cli/dev/cmd_build.py +131 -0
- graph_agents_cli/dev/cmd_install.py +78 -0
- graph_agents_cli/dev/cmd_lint.py +119 -0
- graph_agents_cli/dev/cmd_playground.py +297 -0
- graph_agents_cli/dev/policy_check.py +1287 -0
- graph_agents_cli/eval/__init__.py +22 -0
- graph_agents_cli/eval/_client.py +670 -0
- graph_agents_cli/eval/_common.py +177 -0
- graph_agents_cli/eval/_judge.py +168 -0
- graph_agents_cli/eval/_judge_runner.py +238 -0
- graph_agents_cli/eval/_paths.py +212 -0
- graph_agents_cli/eval/checks.py +581 -0
- graph_agents_cli/eval/cmd_analyze.py +278 -0
- graph_agents_cli/eval/cmd_compare.py +284 -0
- graph_agents_cli/eval/cmd_eval_group.py +80 -0
- graph_agents_cli/eval/cmd_generate.py +558 -0
- graph_agents_cli/eval/cmd_grade.py +466 -0
- graph_agents_cli/eval/cmd_metric.py +156 -0
- graph_agents_cli/eval/cmd_run.py +370 -0
- graph_agents_cli/eval/cmd_submit.py +400 -0
- graph_agents_cli/eval/config.py +435 -0
- graph_agents_cli/eval/dataset.py +350 -0
- graph_agents_cli/eval/gate.py +420 -0
- graph_agents_cli/eval/transcript.py +192 -0
- graph_agents_cli/extension/__init__.py +13 -0
- graph_agents_cli/extension/_compat.py +86 -0
- graph_agents_cli/extension/_loader.py +293 -0
- graph_agents_cli/extension/_manifest.py +135 -0
- graph_agents_cli/extension/_overrides.py +195 -0
- graph_agents_cli/extension/_paths.py +91 -0
- graph_agents_cli/extension/_refs.py +193 -0
- graph_agents_cli/extension/_resolver.py +453 -0
- graph_agents_cli/extension/_schema.py +106 -0
- graph_agents_cli/extension/_spec.py +253 -0
- graph_agents_cli/extension/_sync.py +102 -0
- graph_agents_cli/extension/_trust.py +58 -0
- graph_agents_cli/extension/cmd_extension_add.py +259 -0
- graph_agents_cli/extension/cmd_extension_group.py +57 -0
- graph_agents_cli/extension/cmd_extension_list.py +56 -0
- graph_agents_cli/extension/cmd_extension_remove.py +61 -0
- graph_agents_cli/extension/cmd_extension_update.py +195 -0
- graph_agents_cli/info/__init__.py +13 -0
- graph_agents_cli/info/cmd_info.py +222 -0
- graph_agents_cli/infra/__init__.py +15 -0
- graph_agents_cli/infra/checks.py +1169 -0
- graph_agents_cli/infra/cmd_infra.py +103 -0
- graph_agents_cli/main.py +591 -0
- graph_agents_cli/peer/__init__.py +15 -0
- graph_agents_cli/peer/_generate.py +254 -0
- graph_agents_cli/peer/cmd_peer.py +1151 -0
- graph_agents_cli/run/__init__.py +13 -0
- graph_agents_cli/run/_local_server.py +1157 -0
- graph_agents_cli/run/_signals.py +141 -0
- graph_agents_cli/run/cmd_approvals.py +530 -0
- graph_agents_cli/run/cmd_run.py +1421 -0
- graph_agents_cli/scaffold/__init__.py +19 -0
- graph_agents_cli/scaffold/agents/README.md +24 -0
- graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
- graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
- graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
- graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
- graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
- graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
- graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
- graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
- graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
- graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
- graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
- graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
- graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
- graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
- graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
- graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
- graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
- graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
- graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
- graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
- graph_agents_cli/scaffold/commands/__init__.py +13 -0
- graph_agents_cli/scaffold/commands/create.py +1424 -0
- graph_agents_cli/scaffold/commands/enhance.py +1652 -0
- graph_agents_cli/scaffold/commands/upgrade.py +570 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
- graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
- graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
- graph_agents_cli/scaffold/utils/__init__.py +13 -0
- graph_agents_cli/scaffold/utils/backup.py +212 -0
- graph_agents_cli/scaffold/utils/build_record.py +257 -0
- graph_agents_cli/scaffold/utils/cli_options.py +184 -0
- graph_agents_cli/scaffold/utils/fs.py +83 -0
- graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
- graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
- graph_agents_cli/scaffold/utils/keyedit.py +768 -0
- graph_agents_cli/scaffold/utils/keymerge.py +537 -0
- graph_agents_cli/scaffold/utils/language.py +138 -0
- graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
- graph_agents_cli/scaffold/utils/logging.py +77 -0
- graph_agents_cli/scaffold/utils/manifest.py +292 -0
- graph_agents_cli/scaffold/utils/merge.py +970 -0
- graph_agents_cli/scaffold/utils/merge3.py +216 -0
- graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
- graph_agents_cli/scaffold/utils/remote_template.py +376 -0
- graph_agents_cli/scaffold/utils/template.py +1352 -0
- graph_agents_cli/scaffold/utils/upgrade.py +894 -0
- graph_agents_cli/scaffold/utils/version.py +438 -0
- graph_agents_cli/secrets/__init__.py +15 -0
- graph_agents_cli/secrets/_apply.py +954 -0
- graph_agents_cli/secrets/_required.py +188 -0
- graph_agents_cli/secrets/cmd_secrets.py +211 -0
- graph_agents_cli/setup/__init__.py +13 -0
- graph_agents_cli/setup/_antigravity.py +221 -0
- graph_agents_cli/setup/cmd_auth.py +1030 -0
- graph_agents_cli/setup/cmd_dev_token.py +513 -0
- graph_agents_cli/setup/cmd_setup.py +428 -0
- graph_agents_cli/setup/cmd_update.py +140 -0
- graph_agents_cli/skills/__init__.py +13 -0
- graph_agents_cli/skills/_bundle.py +65 -0
- graph_agents_cli/skills/data/README.md +19 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
- graph_agents_cli/system/__init__.py +15 -0
- graph_agents_cli/system/_apply.py +519 -0
- graph_agents_cli/system/_checks.py +1023 -0
- graph_agents_cli/system/_deploy.py +215 -0
- graph_agents_cli/system/_model.py +363 -0
- graph_agents_cli/system/_system.py +664 -0
- graph_agents_cli/system/_views.py +208 -0
- graph_agents_cli/system/cmd_system.py +423 -0
- graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
- graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
- graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
- graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
- graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
- graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""What the logs never hold: query strings, outbound URLs, raw warnings, a run
|
|
16
|
+
context's credential."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from {{cookiecutter.agent_directory}}.app_utils import telemetry
|
|
31
|
+
|
|
32
|
+
PROJECT = Path(__file__).resolve().parents[2]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def json_logging(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
37
|
+
root = logging.getLogger()
|
|
38
|
+
level, handlers = root.level, list(root.handlers)
|
|
39
|
+
quiet = {name: logging.getLogger(name).level for name in telemetry.QUIET_LOGGERS}
|
|
40
|
+
monkeypatch.setenv("LOG_FORMAT", "json")
|
|
41
|
+
monkeypatch.setenv("LOG_LEVEL", "INFO")
|
|
42
|
+
telemetry.setup_logging()
|
|
43
|
+
yield
|
|
44
|
+
logging.captureWarnings(False)
|
|
45
|
+
root.handlers = handlers
|
|
46
|
+
root.setLevel(level)
|
|
47
|
+
for name, value in quiet.items():
|
|
48
|
+
logging.getLogger(name).setLevel(value)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _records(err: str) -> list[dict]:
|
|
52
|
+
return [json.loads(line) for line in err.splitlines() if line.startswith("{")]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_access_lines_keep_the_path_and_drop_the_query(json_logging, capsys) -> None:
|
|
56
|
+
access = logging.getLogger("uvicorn.access")
|
|
57
|
+
access.info(
|
|
58
|
+
'%s - "%s %s HTTP/%s" %d',
|
|
59
|
+
"127.0.0.1:5000",
|
|
60
|
+
"GET",
|
|
61
|
+
"/threads?access_token=eyJ.canary-token-4411&limit=1",
|
|
62
|
+
"1.1",
|
|
63
|
+
401,
|
|
64
|
+
)
|
|
65
|
+
access.info('%s - "%s %s HTTP/%s" %d', "127.0.0.1:5000", "GET", "/health", "1.1", 200)
|
|
66
|
+
err = capsys.readouterr().err
|
|
67
|
+
assert "canary-token-4411" not in err and "limit=1" not in err
|
|
68
|
+
messages = [r["message"] for r in _records(err)]
|
|
69
|
+
assert messages == [
|
|
70
|
+
'127.0.0.1:5000 - "GET /threads HTTP/1.1" 401',
|
|
71
|
+
'127.0.0.1:5000 - "GET /health HTTP/1.1" 200',
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_http_client_url_lines_stay_out_of_info_logs(json_logging, capsys) -> None:
|
|
76
|
+
logging.getLogger("httpx").info(
|
|
77
|
+
'HTTP Request: GET http://orders.test/orders?customer=alice "HTTP/1.1 200 OK"'
|
|
78
|
+
)
|
|
79
|
+
logging.getLogger("httpcore.connection").info("connect_tcp.started host='orders.test'")
|
|
80
|
+
logging.getLogger("httpx2").info('HTTP Request: POST http://model.test/v1/chat?k=v "200"')
|
|
81
|
+
logging.getLogger("httpx").warning("kept: a warning")
|
|
82
|
+
err = capsys.readouterr().err
|
|
83
|
+
assert "customer=alice" not in err and "connect_tcp" not in err and "k=v" not in err
|
|
84
|
+
assert [r["message"] for r in _records(err)] == ["kept: a warning"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_setup_is_idempotent(json_logging) -> None:
|
|
88
|
+
telemetry.setup_logging()
|
|
89
|
+
access = logging.getLogger("uvicorn.access")
|
|
90
|
+
assert sum(isinstance(f, telemetry.AccessLogFilter) for f in access.filters) == 1
|
|
91
|
+
warns = logging.getLogger("py.warnings")
|
|
92
|
+
assert sum(isinstance(f, telemetry.WarningRedactionFilter) for f in warns.filters) == 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_the_value_a_serializer_warning_echoes_is_redacted() -> None:
|
|
96
|
+
record = logging.LogRecord(
|
|
97
|
+
"py.warnings",
|
|
98
|
+
logging.WARNING,
|
|
99
|
+
__file__,
|
|
100
|
+
1,
|
|
101
|
+
"%s",
|
|
102
|
+
(
|
|
103
|
+
"pydantic/main.py:475: UserWarning: Pydantic serializer warnings:\n"
|
|
104
|
+
" PydanticSerializationUnexpectedValue(Expected `none` - serialized value may "
|
|
105
|
+
"not be as expected [field_name='context', input_value=AgentContext(principal_id"
|
|
106
|
+
"...'secret-TAIL', input_type=AgentContext], input_type=AgentContext])\n"
|
|
107
|
+
" return self.__pydantic_serializer__.to_python(\n",
|
|
108
|
+
),
|
|
109
|
+
None,
|
|
110
|
+
)
|
|
111
|
+
assert telemetry.WarningRedactionFilter().filter(record)
|
|
112
|
+
message = record.getMessage()
|
|
113
|
+
assert "secret-TAIL" not in message and "principal_id" not in message
|
|
114
|
+
assert "input_value=<redacted>, input_type=AgentContext]" in message
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_the_api_client_quiets_the_same_loggers() -> None:
|
|
118
|
+
"""`api_client` quiets them itself (a graph process may never load the app)."""
|
|
119
|
+
import inspect
|
|
120
|
+
|
|
121
|
+
from {{cookiecutter.agent_directory}}.app_utils import api_client
|
|
122
|
+
|
|
123
|
+
names = str(telemetry.QUIET_LOGGERS).replace("'", '"')
|
|
124
|
+
assert names in inspect.getsource(api_client)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- langgraph-server: the server's own lines follow the same rules ------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.fixture
|
|
131
|
+
def server_logging() -> Iterator[logging.Logger]:
|
|
132
|
+
access = logging.getLogger(telemetry.SERVER_ACCESS_LOGGER)
|
|
133
|
+
saved_filters = list(access.filters)
|
|
134
|
+
quiet = {name: logging.getLogger(name).level for name in telemetry.QUIET_LOGGERS}
|
|
135
|
+
for name in telemetry.QUIET_LOGGERS:
|
|
136
|
+
logging.getLogger(name).setLevel(logging.NOTSET)
|
|
137
|
+
telemetry.setup_server_logging()
|
|
138
|
+
yield access
|
|
139
|
+
logging.captureWarnings(False)
|
|
140
|
+
access.filters = saved_filters
|
|
141
|
+
for name, value in quiet.items():
|
|
142
|
+
logging.getLogger(name).setLevel(value)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_the_server_access_line_loses_its_query_string(server_logging) -> None:
|
|
146
|
+
"""LangGraph Server logs a structlog event dict per request, query string included."""
|
|
147
|
+
event = {
|
|
148
|
+
"event": "GET /threads 401 3ms",
|
|
149
|
+
"path": "/threads",
|
|
150
|
+
"status": 401,
|
|
151
|
+
"query_string": "access_token=LGQSCANARY-1&x=1",
|
|
152
|
+
}
|
|
153
|
+
record = logging.LogRecord(
|
|
154
|
+
telemetry.SERVER_ACCESS_LOGGER, logging.WARNING, __file__, 1, event, None, None
|
|
155
|
+
)
|
|
156
|
+
assert all(f.filter(record) for f in server_logging.filters)
|
|
157
|
+
assert record.msg == {k: v for k, v in event.items() if k != "query_string"}
|
|
158
|
+
assert "query_string" in event # the server's own dict is left alone
|
|
159
|
+
telemetry.setup_server_logging() # idempotent
|
|
160
|
+
assert sum(isinstance(f, telemetry.ServerAccessLogFilter) for f in server_logging.filters) == 1
|
|
161
|
+
for name in ("httpx", "httpcore", "httpx2", "httpcore2"):
|
|
162
|
+
assert logging.getLogger(name).getEffectiveLevel() == logging.WARNING
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_the_rendered_server_line_holds_no_query_value(server_logging) -> None:
|
|
166
|
+
"""End to end with structlog configured the way LangGraph Server configures it."""
|
|
167
|
+
structlog = pytest.importorskip("structlog")
|
|
168
|
+
import io
|
|
169
|
+
|
|
170
|
+
stream = io.StringIO()
|
|
171
|
+
handler = logging.StreamHandler(stream)
|
|
172
|
+
handler.setFormatter(
|
|
173
|
+
structlog.stdlib.ProcessorFormatter(
|
|
174
|
+
processors=[
|
|
175
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
176
|
+
structlog.processors.JSONRenderer(),
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
server_logging.addHandler(handler)
|
|
181
|
+
try:
|
|
182
|
+
log = structlog.wrap_logger(
|
|
183
|
+
server_logging,
|
|
184
|
+
processors=[structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
185
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
186
|
+
)
|
|
187
|
+
log.warning("GET /threads 401 3ms", path="/threads", query_string="access_token=CANARY-9")
|
|
188
|
+
finally:
|
|
189
|
+
server_logging.removeHandler(handler)
|
|
190
|
+
line = json.loads(stream.getvalue())
|
|
191
|
+
assert line["event"] == "GET /threads 401 3ms" and line["path"] == "/threads"
|
|
192
|
+
assert "CANARY-9" not in stream.getvalue() and "query_string" not in line
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# A tool with the unparameterised `ToolRuntime` makes pydantic warn with the run
|
|
196
|
+
# context's repr on every call. The run context holds the caller's forwarded
|
|
197
|
+
# credential; the process's stderr must stay JSON and must not hold it.
|
|
198
|
+
_TOOL_RUN = """
|
|
199
|
+
import asyncio
|
|
200
|
+
from dataclasses import dataclass, field
|
|
201
|
+
from typing import Any
|
|
202
|
+
|
|
203
|
+
from langchain.agents import create_agent
|
|
204
|
+
from langchain.tools import ToolRuntime
|
|
205
|
+
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
|
|
206
|
+
from langchain_core.messages import AIMessage
|
|
207
|
+
from langchain_core.tools import tool
|
|
208
|
+
|
|
209
|
+
from {{cookiecutter.agent_directory}}.app_utils import telemetry
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class Context:
|
|
214
|
+
principal_id: str = "anonymous"
|
|
215
|
+
roles: list[str] = field(default_factory=list)
|
|
216
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Scripted(FakeMessagesListChatModel):
|
|
220
|
+
def bind_tools(self, tools, **kwargs):
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@tool
|
|
225
|
+
async def lookup(item: str, runtime: ToolRuntime) -> str:
|
|
226
|
+
\"\"\"Look up ITEM.\"\"\"
|
|
227
|
+
return "found " + item
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def main() -> None:
|
|
231
|
+
telemetry.setup_logging()
|
|
232
|
+
model = Scripted(responses=[
|
|
233
|
+
AIMessage(content="", tool_calls=[{"name": "lookup", "args": {"item": "x"}, "id": "c1"}]),
|
|
234
|
+
AIMessage(content="done"),
|
|
235
|
+
])
|
|
236
|
+
graph = create_agent(model=model, tools=[lookup], context_schema=Context)
|
|
237
|
+
context = Context(
|
|
238
|
+
principal_id="alice@example.com",
|
|
239
|
+
roles=["user"],
|
|
240
|
+
attributes={"credentials": {"orders": "FWD-SECRET-abcdefghijklmnop-TAILMARK"}},
|
|
241
|
+
)
|
|
242
|
+
async for _ in graph.astream(
|
|
243
|
+
{"messages": [{"role": "user", "content": "go"}]},
|
|
244
|
+
context=context,
|
|
245
|
+
stream_mode=["updates"],
|
|
246
|
+
):
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
asyncio.run(main())
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_warnings_are_json_records_without_the_context_repr() -> None:
|
|
255
|
+
env = dict(os.environ)
|
|
256
|
+
env.update({"LOG_FORMAT": "json", "LOG_LEVEL": "INFO", "PYTHONWARNINGS": "default"})
|
|
257
|
+
result = subprocess.run(
|
|
258
|
+
[sys.executable, "-c", _TOOL_RUN],
|
|
259
|
+
env=env,
|
|
260
|
+
cwd=PROJECT,
|
|
261
|
+
capture_output=True,
|
|
262
|
+
text=True,
|
|
263
|
+
timeout=120,
|
|
264
|
+
)
|
|
265
|
+
assert result.returncode == 0, result.stderr
|
|
266
|
+
assert "TAILMARK" not in result.stderr and "alice@example.com" not in result.stderr
|
|
267
|
+
lines = [line for line in result.stderr.splitlines() if line.strip()]
|
|
268
|
+
records = [json.loads(line) for line in lines] # every line is one JSON record
|
|
269
|
+
# The warning itself is kept (a JSON record); only the value it echoes goes.
|
|
270
|
+
warned = [r["message"] for r in records if r["logger"] == "py.warnings"]
|
|
271
|
+
assert warned and all("input_value=<redacted>" in m for m in warned if "input_value" in m)
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Unit tests for the auth policies: shared-bearer, the custom stub and policy selection.
|
|
16
|
+
|
|
17
|
+
The jwt policy has its own file (test_jwt_policy.py). No network, no model key.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
from fastapi import HTTPException
|
|
24
|
+
from starlette.requests import Request
|
|
25
|
+
|
|
26
|
+
from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
|
|
27
|
+
from {{cookiecutter.agent_directory}}.app_utils.auth import (
|
|
28
|
+
ACTIONS,
|
|
29
|
+
Actor,
|
|
30
|
+
JwtPolicy,
|
|
31
|
+
Principal,
|
|
32
|
+
SharedBearerPolicy,
|
|
33
|
+
actor_from_claims,
|
|
34
|
+
admin_roles,
|
|
35
|
+
authenticate_and_authorize,
|
|
36
|
+
check_startup,
|
|
37
|
+
delegation_settings,
|
|
38
|
+
dev_mode,
|
|
39
|
+
finalize_principal,
|
|
40
|
+
get_policy,
|
|
41
|
+
keep_subject_token,
|
|
42
|
+
policy_name,
|
|
43
|
+
read_across_roles,
|
|
44
|
+
require,
|
|
45
|
+
reset_policy_cache,
|
|
46
|
+
)
|
|
47
|
+
from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
|
|
48
|
+
from {{cookiecutter.agent_directory}}.policies import build_policy
|
|
49
|
+
from {{cookiecutter.agent_directory}}.policies.custom import NOT_IMPLEMENTED, CustomPolicy
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _request(headers: dict[str, str] | None = None) -> Request:
|
|
53
|
+
raw = [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()]
|
|
54
|
+
return Request(
|
|
55
|
+
{"type": "http", "method": "POST", "path": "/chat", "headers": raw, "query_string": b""}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- SharedBearerPolicy -------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def test_shared_bearer_accepts_the_configured_key() -> None:
|
|
63
|
+
policy = SharedBearerPolicy(api_key="s3cret")
|
|
64
|
+
principal = await policy.authenticate(_request({"Authorization": "Bearer s3cret"}))
|
|
65
|
+
assert principal.id == "shared"
|
|
66
|
+
assert principal.permissions == set(ACTIONS)
|
|
67
|
+
assert len(principal.hashed_id()) == 16
|
|
68
|
+
for action in ACTIONS:
|
|
69
|
+
await policy.authorize(principal, action, None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize(
|
|
73
|
+
"headers",
|
|
74
|
+
[
|
|
75
|
+
{},
|
|
76
|
+
{"Authorization": "Bearer wrong"},
|
|
77
|
+
{"Authorization": "Basic s3cret"},
|
|
78
|
+
{"Authorization": "Bearer"},
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
async def test_shared_bearer_rejects_missing_or_wrong_key(headers: dict[str, str]) -> None:
|
|
82
|
+
policy = SharedBearerPolicy(api_key="s3cret")
|
|
83
|
+
with pytest.raises(HTTPException) as exc:
|
|
84
|
+
await policy.authenticate(_request(headers))
|
|
85
|
+
assert exc.value.status_code == 401
|
|
86
|
+
assert exc.value.headers == {"WWW-Authenticate": "Bearer"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def test_shared_bearer_fails_closed_without_a_configured_key(
|
|
90
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
91
|
+
) -> None:
|
|
92
|
+
monkeypatch.delenv("API_KEY", raising=False)
|
|
93
|
+
policy = SharedBearerPolicy()
|
|
94
|
+
with pytest.raises(HTTPException) as exc:
|
|
95
|
+
await policy.authenticate(_request({"Authorization": "Bearer anything"}))
|
|
96
|
+
assert exc.value.status_code == 503
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def test_shared_bearer_reads_api_key_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
100
|
+
monkeypatch.setenv("API_KEY", "from-env")
|
|
101
|
+
principal = await SharedBearerPolicy().authenticate(
|
|
102
|
+
_request({"Authorization": "Bearer from-env"})
|
|
103
|
+
)
|
|
104
|
+
assert principal.id == "shared"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def test_unknown_action_is_forbidden() -> None:
|
|
108
|
+
policy = SharedBearerPolicy(api_key="k")
|
|
109
|
+
with pytest.raises(HTTPException) as exc:
|
|
110
|
+
await policy.authorize(Principal(id="shared"), "thread.export", None)
|
|
111
|
+
assert exc.value.status_code == 403
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --- CustomPolicy stub and an unconfigured jwt policy: fail closed ---------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.fixture
|
|
118
|
+
def custom_policy(monkeypatch: pytest.MonkeyPatch):
|
|
119
|
+
monkeypatch.setenv("AUTH_POLICY", "custom")
|
|
120
|
+
reset_policy_cache()
|
|
121
|
+
yield
|
|
122
|
+
reset_policy_cache()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def test_custom_stub_fails_closed_on_every_action(custom_policy) -> None:
|
|
126
|
+
assert isinstance(build_policy("custom"), CustomPolicy)
|
|
127
|
+
assert isinstance(get_policy(), CustomPolicy)
|
|
128
|
+
with pytest.raises(HTTPException) as exc:
|
|
129
|
+
await require("chat.send")(_request({"Authorization": "Bearer test-key"}))
|
|
130
|
+
assert exc.value.status_code == 503 and exc.value.detail == NOT_IMPLEMENTED
|
|
131
|
+
for action in ACTIONS:
|
|
132
|
+
with pytest.raises(HTTPException) as exc:
|
|
133
|
+
await authenticate_and_authorize(_request(), action)
|
|
134
|
+
assert exc.value.status_code == 503
|
|
135
|
+
# A stub that only failed authenticate would still be caught here.
|
|
136
|
+
with pytest.raises(HTTPException) as exc:
|
|
137
|
+
await CustomPolicy().authorize(Principal(id="u1"), "chat.send", None)
|
|
138
|
+
assert exc.value.status_code == 503
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def test_unconfigured_jwt_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
142
|
+
monkeypatch.setenv("AUTH_POLICY", "jwt")
|
|
143
|
+
monkeypatch.setenv("APP_ENV", "dev")
|
|
144
|
+
for name in ("AUTH_JWT_JWKS_URL", "AUTH_JWT_PUBLIC_KEY", "AUTH_JWT_ALGORITHMS"):
|
|
145
|
+
monkeypatch.delenv(name, raising=False)
|
|
146
|
+
reset_policy_cache()
|
|
147
|
+
try:
|
|
148
|
+
assert isinstance(get_policy(), JwtPolicy)
|
|
149
|
+
for action in ACTIONS:
|
|
150
|
+
with pytest.raises(HTTPException) as exc:
|
|
151
|
+
await authenticate_and_authorize(
|
|
152
|
+
_request({"Authorization": "Bearer x.y.z"}), action
|
|
153
|
+
)
|
|
154
|
+
assert exc.value.status_code == 503
|
|
155
|
+
assert "AUTH_POLICY=jwt" in exc.value.detail
|
|
156
|
+
finally:
|
|
157
|
+
reset_policy_cache()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# --- startup check and shared settings ------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_check_startup(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
164
|
+
for app_env in ("dev", "prod"):
|
|
165
|
+
monkeypatch.setenv("APP_ENV", app_env)
|
|
166
|
+
# shared-bearer and the custom stub have nothing to check at startup.
|
|
167
|
+
for name in ("shared-bearer", "custom", "product-session"):
|
|
168
|
+
monkeypatch.setenv("AUTH_POLICY", name)
|
|
169
|
+
reset_policy_cache()
|
|
170
|
+
check_startup()
|
|
171
|
+
# A typo in AUTH_POLICY never starts, in any environment.
|
|
172
|
+
monkeypatch.setenv("AUTH_POLICY", "jtw")
|
|
173
|
+
reset_policy_cache()
|
|
174
|
+
with pytest.raises(RuntimeError, match="Unknown AUTH_POLICY"):
|
|
175
|
+
check_startup()
|
|
176
|
+
reset_policy_cache()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_dev_mode_needs_an_explicit_app_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
180
|
+
# Only exactly `dev`: dev relaxes checks, so a near miss is a deployed environment.
|
|
181
|
+
for value, expected in (
|
|
182
|
+
("dev", True),
|
|
183
|
+
(" DEV ", False),
|
|
184
|
+
("Dev", False),
|
|
185
|
+
("dev ", False),
|
|
186
|
+
("development", False),
|
|
187
|
+
("prod", False),
|
|
188
|
+
("", False),
|
|
189
|
+
):
|
|
190
|
+
monkeypatch.setenv("APP_ENV", value)
|
|
191
|
+
assert dev_mode() is expected, value
|
|
192
|
+
monkeypatch.delenv("APP_ENV")
|
|
193
|
+
assert dev_mode() is False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_role_lists(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
197
|
+
monkeypatch.delenv("AUTH_ADMIN_ROLES", raising=False)
|
|
198
|
+
assert admin_roles() == set() # empty = nobody
|
|
199
|
+
monkeypatch.setenv("AUTH_ADMIN_ROLES", " admin, ,platform ")
|
|
200
|
+
monkeypatch.setenv("AUTH_READ_ACROSS_ROLES", "support")
|
|
201
|
+
assert admin_roles() == {"admin", "platform"}
|
|
202
|
+
assert read_across_roles() == {"support"}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_retired_policy_name_reads_as_custom(
|
|
206
|
+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
207
|
+
) -> None:
|
|
208
|
+
monkeypatch.setenv("AUTH_POLICY", "product-session")
|
|
209
|
+
monkeypatch.setattr(auth_module, "_warned_aliases", set())
|
|
210
|
+
reset_policy_cache()
|
|
211
|
+
try:
|
|
212
|
+
with caplog.at_level("WARNING"):
|
|
213
|
+
assert policy_name() == "custom"
|
|
214
|
+
assert policy_name() == "custom"
|
|
215
|
+
assert isinstance(get_policy(), CustomPolicy)
|
|
216
|
+
warnings = [r for r in caplog.records if "deprecated" in r.getMessage()]
|
|
217
|
+
assert len(warnings) == 1
|
|
218
|
+
finally:
|
|
219
|
+
reset_policy_cache()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_public_attributes_never_carry_credentials() -> None:
|
|
223
|
+
principal = Principal(
|
|
224
|
+
id="u1", attributes={"tenant": "t1", "credentials": {"billing": "Bearer secret"}}
|
|
225
|
+
)
|
|
226
|
+
assert principal.public_attributes() == {"tenant": "t1"}
|
|
227
|
+
assert "credentials" in principal.attributes # the original is untouched
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_hashed_id_is_salted_with_principal_hash_salt(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
231
|
+
import hashlib
|
|
232
|
+
import hmac
|
|
233
|
+
|
|
234
|
+
principal = Principal(id="alice@example.com")
|
|
235
|
+
monkeypatch.delenv("PRINCIPAL_HASH_SALT", raising=False)
|
|
236
|
+
unsalted = principal.hashed_id()
|
|
237
|
+
assert unsalted == hashlib.sha256(b"alice@example.com").hexdigest()[:16]
|
|
238
|
+
monkeypatch.setenv("PRINCIPAL_HASH_SALT", " ") # blank = not set
|
|
239
|
+
assert principal.hashed_id() == unsalted
|
|
240
|
+
monkeypatch.setenv("PRINCIPAL_HASH_SALT", "pepper-1")
|
|
241
|
+
salted = principal.hashed_id()
|
|
242
|
+
expected = hmac.new(b"pepper-1", b"alice@example.com", hashlib.sha256).hexdigest()[:16]
|
|
243
|
+
assert salted == expected and salted != unsalted and len(salted) == 16
|
|
244
|
+
assert Principal(id="bob").hashed_id() != salted
|
|
245
|
+
monkeypatch.setenv("PRINCIPAL_HASH_SALT", "pepper-2")
|
|
246
|
+
assert principal.hashed_id() not in (salted, unsalted)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_unknown_policy_name_is_an_error() -> None:
|
|
250
|
+
with pytest.raises(RuntimeError, match="Unknown AUTH_POLICY"):
|
|
251
|
+
build_policy("bogus")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def test_not_implemented_error_from_a_policy_maps_to_503(
|
|
255
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
256
|
+
) -> None:
|
|
257
|
+
assert auth_module._as_http_exception(NotImplementedError("x")).status_code == 503
|
|
258
|
+
assert auth_module._as_http_exception(NotImplementedError()).detail.startswith("The configured")
|
|
259
|
+
kept = HTTPException(status_code=401)
|
|
260
|
+
assert auth_module._as_http_exception(kept) is kept
|
|
261
|
+
with pytest.raises(ValueError):
|
|
262
|
+
auth_module._as_http_exception(ValueError("not mapped"))
|
|
263
|
+
|
|
264
|
+
class _Unimplemented:
|
|
265
|
+
async def authenticate(self, request: Request) -> Principal:
|
|
266
|
+
raise NotImplementedError("consumer policy pending")
|
|
267
|
+
|
|
268
|
+
async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
|
|
269
|
+
raise NotImplementedError
|
|
270
|
+
|
|
271
|
+
monkeypatch.setattr(auth_module, "get_policy", lambda: _Unimplemented())
|
|
272
|
+
with pytest.raises(HTTPException) as exc:
|
|
273
|
+
await require("chat.send")(_request())
|
|
274
|
+
assert exc.value.status_code == 503 and "pending" in str(exc.value.detail)
|
|
275
|
+
with pytest.raises(HTTPException) as exc:
|
|
276
|
+
await authenticate_and_authorize(_request(), "card.read")
|
|
277
|
+
assert exc.value.status_code == 503
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# --- finalize_principal: one rule set for every policy (0.3) --------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class _FixedPolicy:
|
|
284
|
+
"""A custom policy that returns one principal, as a project's own policy would."""
|
|
285
|
+
|
|
286
|
+
def __init__(self, principal: Principal) -> None:
|
|
287
|
+
self.principal = principal
|
|
288
|
+
|
|
289
|
+
async def authenticate(self, request: Request) -> Principal:
|
|
290
|
+
return self.principal
|
|
291
|
+
|
|
292
|
+
async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
async def test_shared_bearer_principals_pass_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
297
|
+
monkeypatch.setenv("AUTH_POLICY", "shared-bearer")
|
|
298
|
+
monkeypatch.setenv("API_KEY", "k")
|
|
299
|
+
reset_policy_cache()
|
|
300
|
+
try:
|
|
301
|
+
principal = await require("chat.send")(_request({"Authorization": "Bearer k"}))
|
|
302
|
+
finally:
|
|
303
|
+
reset_policy_cache()
|
|
304
|
+
assert (principal.id, principal.roles, principal.actor) == ("shared", ["shared"], None)
|
|
305
|
+
assert principal.owner_key() == "shared" and "@actor" not in principal.attributes
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@pytest.mark.parametrize("bad_id", ["", "a\x00b", "tab\there", "x" * 257])
|
|
309
|
+
async def test_a_custom_policys_invalid_ids_are_refused_as_its_bug(
|
|
310
|
+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, bad_id: str
|
|
311
|
+
) -> None:
|
|
312
|
+
monkeypatch.setenv("AUTH_POLICY", "custom")
|
|
313
|
+
for principal in (
|
|
314
|
+
Principal(id=bad_id),
|
|
315
|
+
Principal(id="alice", actor=Actor(id=bad_id)),
|
|
316
|
+
Principal(id="alice", actor=Actor(id="concierge", chain=("concierge", bad_id))),
|
|
317
|
+
):
|
|
318
|
+
monkeypatch.setattr(auth_module, "get_policy", lambda p=principal: _FixedPolicy(p))
|
|
319
|
+
with pytest.raises(HTTPException) as exc:
|
|
320
|
+
await authenticate_and_authorize(_request(), "a2a.invoke")
|
|
321
|
+
assert exc.value.status_code == 500
|
|
322
|
+
assert "a bug in the policy" in caplog.text
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def test_a_custom_policy_marks_delegation_with_actor(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
326
|
+
monkeypatch.setenv("AUTH_POLICY", "custom")
|
|
327
|
+
delegated = Principal(
|
|
328
|
+
id="alice", roles=["support"], actor=Actor(id="concierge"), permissions=set(ACTIONS)
|
|
329
|
+
)
|
|
330
|
+
monkeypatch.setattr(auth_module, "get_policy", lambda: _FixedPolicy(delegated))
|
|
331
|
+
with pytest.raises(HTTPException) as exc:
|
|
332
|
+
await require("chat.send")(_request())
|
|
333
|
+
assert exc.value.status_code == 403
|
|
334
|
+
monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "concierge")
|
|
335
|
+
principal = await require("chat.send")(_request())
|
|
336
|
+
assert principal.actor == Actor(id="concierge", chain=("concierge",))
|
|
337
|
+
assert principal.roles == [] and principal.owner_key() == "alice\x1fconcierge"
|
|
338
|
+
# A chain that does not start with the actor is a bug in the policy.
|
|
339
|
+
broken = Principal(id="alice", actor=Actor(id="concierge", chain=("billing",)))
|
|
340
|
+
monkeypatch.setattr(auth_module, "get_policy", lambda: _FixedPolicy(broken))
|
|
341
|
+
with pytest.raises(HTTPException) as exc:
|
|
342
|
+
await require("chat.send")(_request())
|
|
343
|
+
assert exc.value.status_code == 500
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_the_jwt_helpers_are_exported_for_custom_policies() -> None:
|
|
347
|
+
actor = actor_from_claims({"sub": "alice", "act": {"sub": "concierge"}})
|
|
348
|
+
assert actor == Actor(id="concierge", chain=("concierge",), client=None)
|
|
349
|
+
principal = keep_subject_token(Principal(id="alice"), "tok", ["a", "b"])
|
|
350
|
+
assert principal.attributes["credentials"] == {
|
|
351
|
+
"@subject_token": "tok",
|
|
352
|
+
"@subject_aud": ("a", "b"),
|
|
353
|
+
}
|
|
354
|
+
assert principal.public_attributes() == {}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_delegation_settings_are_checked(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
358
|
+
defaults = delegation_settings({})
|
|
359
|
+
assert (defaults.max_depth, defaults.allowed_actors, defaults.delegated_roles) == (
|
|
360
|
+
3,
|
|
361
|
+
frozenset(),
|
|
362
|
+
frozenset(),
|
|
363
|
+
)
|
|
364
|
+
assert not defaults.allows("concierge")
|
|
365
|
+
assert delegation_settings({"AUTH_ALLOWED_ACTORS": "*"}).allows("anyone")
|
|
366
|
+
for env in (
|
|
367
|
+
{"AUTH_MAX_DELEGATION_DEPTH": "0"},
|
|
368
|
+
{"AUTH_MAX_DELEGATION_DEPTH": "9"},
|
|
369
|
+
{"AUTH_MAX_DELEGATION_DEPTH": "three"},
|
|
370
|
+
{"AUTH_ALLOWED_ACTORS": "concierge, bad\x01id"},
|
|
371
|
+
):
|
|
372
|
+
with pytest.raises(SettingsError):
|
|
373
|
+
delegation_settings(env)
|
|
374
|
+
# A bad value reached at request time (the startup check refuses it) fails closed.
|
|
375
|
+
monkeypatch.setenv("AUTH_MAX_DELEGATION_DEPTH", "99")
|
|
376
|
+
with pytest.raises(HTTPException) as exc:
|
|
377
|
+
finalize_principal(Principal(id="alice"))
|
|
378
|
+
assert exc.value.status_code == 503
|