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,234 @@
|
|
|
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
|
+
"""Deployment mode derivation (direct, argocd, helm-push) and kube context resolution.
|
|
15
|
+
|
|
16
|
+
The kube context decides which cluster every kubectl and helm call reaches.
|
|
17
|
+
Taking it silently from the kubeconfig's current context means a developer
|
|
18
|
+
whose terminal points at production can deploy there by accident (and a prod
|
|
19
|
+
rollout can land on a laptop cluster). So only ``dev`` may use the current
|
|
20
|
+
context without asking: every other environment needs the context recorded in
|
|
21
|
+
the manifest (``environments.<env>.context``) or passed with ``--context``, or
|
|
22
|
+
an explicit confirmation (a prompt at a terminal, ``--yes`` otherwise). The
|
|
23
|
+
resolved context and its API server are printed before anything happens.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
|
|
33
|
+
from graph_agents_cli._output import Console
|
|
34
|
+
from graph_agents_cli.deploy import _kube
|
|
35
|
+
from graph_agents_cli.deploy._config import DeploySettings
|
|
36
|
+
from graph_agents_cli.deploy._kube import ConfigError, Refused
|
|
37
|
+
|
|
38
|
+
LOCAL_LOAD = "local-load"
|
|
39
|
+
REGISTRY = "registry"
|
|
40
|
+
HELM_PUSH = "helm-push"
|
|
41
|
+
ARGOCD = "argocd"
|
|
42
|
+
|
|
43
|
+
DIRECT_MODES = (LOCAL_LOAD, REGISTRY)
|
|
44
|
+
|
|
45
|
+
# Environments with policy gates (auth policy implemented, helm-push CI-only deploys).
|
|
46
|
+
PROTECTED_ENVS = ("staging", "prod")
|
|
47
|
+
# The one environment that may fall back to `.env` and the current kube context.
|
|
48
|
+
DEV_ENV = "dev"
|
|
49
|
+
|
|
50
|
+
# Kube context names that suggest a single-node dev cluster (kind, k3s, minikube, ...).
|
|
51
|
+
DEV_CONTEXT_PREFIXES = ("kind-", "k3d-", "minikube", "k3s")
|
|
52
|
+
DEV_CONTEXT_NAMES = ("minikube", "docker-desktop", "rancher-desktop", "orbstack", "k3s")
|
|
53
|
+
|
|
54
|
+
FLAG = "flag"
|
|
55
|
+
MANIFEST = "manifest"
|
|
56
|
+
CURRENT = "current"
|
|
57
|
+
NONE = "none"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_dev_cluster(context: str | None) -> bool:
|
|
61
|
+
"""Whether a context *name* looks like a dev cluster (a hint; see ``local_load.detect``)."""
|
|
62
|
+
if not context:
|
|
63
|
+
return False
|
|
64
|
+
name = context.strip()
|
|
65
|
+
return name in DEV_CONTEXT_NAMES or name.startswith(DEV_CONTEXT_PREFIXES)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_dev_env(env: str) -> bool:
|
|
69
|
+
return env == DEV_ENV
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ResolvedContext:
|
|
74
|
+
"""The kube context for one command and where it came from."""
|
|
75
|
+
|
|
76
|
+
name: str | None
|
|
77
|
+
source: str # FLAG, MANIFEST, CURRENT or NONE
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def explicit(self) -> bool:
|
|
81
|
+
return self.source in (FLAG, MANIFEST)
|
|
82
|
+
|
|
83
|
+
def describe(self, env: str) -> str:
|
|
84
|
+
return {
|
|
85
|
+
FLAG: "from --context",
|
|
86
|
+
MANIFEST: f"from environments.{env}.context",
|
|
87
|
+
CURRENT: "the kubeconfig's current context",
|
|
88
|
+
NONE: "no context recorded and no current context",
|
|
89
|
+
}[self.source]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve(settings: DeploySettings, env: str, override: str | None = None) -> ResolvedContext:
|
|
93
|
+
"""``--context``, else the manifest's ``environments.<env>.context``, else the current one."""
|
|
94
|
+
if override and override.strip():
|
|
95
|
+
return ResolvedContext(override.strip(), FLAG)
|
|
96
|
+
recorded = settings.environment(env).get("context")
|
|
97
|
+
if recorded:
|
|
98
|
+
return ResolvedContext(recorded, MANIFEST)
|
|
99
|
+
current = _kube.current_context()
|
|
100
|
+
if current:
|
|
101
|
+
return ResolvedContext(current, CURRENT)
|
|
102
|
+
return ResolvedContext(None, NONE)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def resolve_context(settings: DeploySettings, env: str) -> str | None:
|
|
106
|
+
"""The environment's recorded context, else the kubeconfig's current context."""
|
|
107
|
+
return resolve(settings, env).name
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _interactive() -> bool:
|
|
111
|
+
try:
|
|
112
|
+
return sys.stdin.isatty()
|
|
113
|
+
except (AttributeError, ValueError):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def announce(
|
|
118
|
+
env: str, resolved: ResolvedContext, *, console: Console, show_server: bool = True
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Print the context (and its API server) every cluster-touching command will use."""
|
|
121
|
+
server = _kube.server_url(resolved.name) if show_server and resolved.name else None
|
|
122
|
+
where = f"; server {server}" if server else ""
|
|
123
|
+
console.print(
|
|
124
|
+
f"Kube context: {resolved.name or '(none)'} ({resolved.describe(env)}{where})",
|
|
125
|
+
highlight=False,
|
|
126
|
+
markup=False,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def require_known_context(
|
|
131
|
+
env: str, resolved: ResolvedContext, *, dry_run: bool, console: Console
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Exit 3 when an explicit context (``--context`` or the manifest) is not in the kubeconfig.
|
|
134
|
+
|
|
135
|
+
A typo is a configuration error, reported the same way by every command
|
|
136
|
+
(read-only ones included), not a kubectl failure (exit 2) that a gate script
|
|
137
|
+
could mistake for an unreachable cluster. ``--dry-run`` only reports it.
|
|
138
|
+
"""
|
|
139
|
+
if not (resolved.explicit and resolved.name):
|
|
140
|
+
return
|
|
141
|
+
names = _kube.context_names()
|
|
142
|
+
if not names or resolved.name in names:
|
|
143
|
+
return
|
|
144
|
+
message = (
|
|
145
|
+
f"Kube context {resolved.name!r} ({resolved.describe(env)}) is not in the "
|
|
146
|
+
f"kubeconfig. Known contexts: {', '.join(sorted(names)[:10])}."
|
|
147
|
+
)
|
|
148
|
+
if not dry_run:
|
|
149
|
+
raise ConfigError(message)
|
|
150
|
+
console.print(f" [dry-run] {message}", style="yellow", markup=False)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def confirm(
|
|
154
|
+
env: str,
|
|
155
|
+
resolved: ResolvedContext,
|
|
156
|
+
*,
|
|
157
|
+
yes: bool,
|
|
158
|
+
dry_run: bool,
|
|
159
|
+
console: Console,
|
|
160
|
+
action: str,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Refuse or confirm an implicit context for any environment other than ``dev``.
|
|
163
|
+
|
|
164
|
+
``dev`` and an explicit context (``--context`` or the manifest) pass, once
|
|
165
|
+
the kubeconfig is known to hold that context (checked here, so a typo stops
|
|
166
|
+
the command before anything is built). For another environment an implicit
|
|
167
|
+
current context needs ``--yes`` or a ``y`` at the prompt; with no context at
|
|
168
|
+
all it is a configuration error. ``--dry-run`` never prompts (nothing is
|
|
169
|
+
changed) and only reports.
|
|
170
|
+
"""
|
|
171
|
+
require_known_context(env, resolved, dry_run=dry_run, console=console)
|
|
172
|
+
if is_dev_env(env) or resolved.explicit:
|
|
173
|
+
return
|
|
174
|
+
how = (
|
|
175
|
+
f"Record it as environments.{env}.context in graph-agents-cli-manifest.yaml or pass "
|
|
176
|
+
"--context <name>"
|
|
177
|
+
)
|
|
178
|
+
if resolved.name is None:
|
|
179
|
+
if dry_run:
|
|
180
|
+
console.print(
|
|
181
|
+
f" [dry-run] no kube context for {env}; the real run stops here. {how}.",
|
|
182
|
+
style="yellow",
|
|
183
|
+
markup=False,
|
|
184
|
+
)
|
|
185
|
+
return
|
|
186
|
+
raise ConfigError(f"No kube context for {env}. {how}.")
|
|
187
|
+
if dry_run:
|
|
188
|
+
console.print(
|
|
189
|
+
f" [dry-run] {env} has no recorded context; the real run asks to confirm "
|
|
190
|
+
f"{resolved.name!r} (or needs --yes).",
|
|
191
|
+
style="yellow",
|
|
192
|
+
markup=False,
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
if yes:
|
|
196
|
+
console.print(
|
|
197
|
+
f" Using the current context {resolved.name!r} for {env} (--yes).",
|
|
198
|
+
style="yellow",
|
|
199
|
+
markup=False,
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
if not _interactive():
|
|
203
|
+
raise Refused(
|
|
204
|
+
f"Refusing to {action} {env} on the kubeconfig's current context {resolved.name!r} "
|
|
205
|
+
f"without confirmation.\n {how}, or pass --yes to accept the current context."
|
|
206
|
+
)
|
|
207
|
+
# Only the first letter changes: `str.capitalize` would lowercase "Secret".
|
|
208
|
+
if not click.confirm(
|
|
209
|
+
f"{action[:1].upper()}{action[1:]} {env} on context {resolved.name!r}?", default=False
|
|
210
|
+
):
|
|
211
|
+
raise Refused(f"Aborted: {env} was not changed. {how}.")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def derive_mode(cd: str, context: str | None, *, local: bool | None = None) -> str:
|
|
215
|
+
"""The deploy mode for ``cd``; ``local`` (when known) overrides the context-name guess."""
|
|
216
|
+
if cd == "skip":
|
|
217
|
+
is_local = is_dev_cluster(context) if local is None else local
|
|
218
|
+
return LOCAL_LOAD if is_local else REGISTRY
|
|
219
|
+
if cd == HELM_PUSH:
|
|
220
|
+
return HELM_PUSH
|
|
221
|
+
if cd == ARGOCD:
|
|
222
|
+
return ARGOCD
|
|
223
|
+
raise ConfigError(
|
|
224
|
+
f"Unknown cd value {cd!r} in the manifest (expected argocd, helm-push or skip)."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def describe(mode: str) -> str:
|
|
229
|
+
return {
|
|
230
|
+
LOCAL_LOAD: "direct, local-load (build and load the image into the local cluster)",
|
|
231
|
+
REGISTRY: "direct, registry (build, push, and helm upgrade)",
|
|
232
|
+
HELM_PUSH: "helm-push (CI builds and pushes; deploy runs helm)",
|
|
233
|
+
ARGOCD: "argocd (deploy writes desired state; Argo CD reconciles from main)",
|
|
234
|
+
}.get(mode, mode)
|
|
@@ -0,0 +1,370 @@
|
|
|
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
|
+
"""Settings checks shared by ``deploy`` and ``infra check``: what the pods would see.
|
|
15
|
+
|
|
16
|
+
A pod's environment is the app Secret (``envFrom``), then the chart's ConfigMap
|
|
17
|
+
(``env`` in the merged values, also ``envFrom`` and listed after the Secret, so
|
|
18
|
+
it wins, even with an empty value), then the variables the chart sets itself.
|
|
19
|
+
These checks read that environment the same way, from the merged chart values
|
|
20
|
+
and the names of the keys the Secret holds (values are never needed):
|
|
21
|
+
|
|
22
|
+
- ``env_placeholders``: a scaffold placeholder (``CHANGE-ME``) still in the
|
|
23
|
+
chart env, for example an API base URL: the pods would call it.
|
|
24
|
+
- ``jwt_findings``: the ``jwt`` policy without a verification key, issuer or
|
|
25
|
+
audience. Outside ``APP_ENV=dev`` the pod refuses to start without them (after
|
|
26
|
+
the image was built and the rollout waited); in dev it starts, reports Ready
|
|
27
|
+
and answers every request with 503.
|
|
28
|
+
- ``dsn_without_tls``: a database connection string for an external database
|
|
29
|
+
that does not require TLS.
|
|
30
|
+
- ``peer_findings``: the other agents the project calls (``protocol: a2a`` APIs):
|
|
31
|
+
a peer reached with a credential over plain http to a host that is not
|
|
32
|
+
cluster-internal, an ``auth: exchange`` API without the issuer's token URL
|
|
33
|
+
or this agent's client id (or a plain-http token URL), and what is missing
|
|
34
|
+
from ``secrets.keys``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import ipaddress
|
|
40
|
+
import re
|
|
41
|
+
import urllib.parse
|
|
42
|
+
from collections.abc import Mapping, Set
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
from graph_agents_cli._defaults import normalize_auth_policy
|
|
47
|
+
from graph_agents_cli.deploy import _image, _modes
|
|
48
|
+
from graph_agents_cli.deploy._config import DeploySettings
|
|
49
|
+
|
|
50
|
+
JWT_KEY_SOURCES = ("AUTH_JWT_JWKS_URL", "AUTH_JWT_PUBLIC_KEY")
|
|
51
|
+
JWT_REQUIRED_OUTSIDE_DEV = ("AUTH_JWT_ISSUER", "AUTH_JWT_AUDIENCE")
|
|
52
|
+
_DEFAULT_JWT_ALGORITHMS = "RS256,ES256"
|
|
53
|
+
_HMAC = frozenset({"HS256", "HS384", "HS512"})
|
|
54
|
+
# libpq modes that refuse a connection without TLS.
|
|
55
|
+
TLS_SSLMODES = frozenset({"require", "verify-ca", "verify-full"})
|
|
56
|
+
# URL (`?sslmode=x`) or keyword/value form, where libpq allows spaces around `=`
|
|
57
|
+
# and a quoted value (`sslmode = 'verify-full'`).
|
|
58
|
+
_SSLMODE = re.compile(r"(?:^|[?&\s])sslmode\s*=\s*'?([A-Za-z-]+)")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def chart_env(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
62
|
+
env = values.get("env")
|
|
63
|
+
return dict(env) if isinstance(env, Mapping) else {}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def env_placeholders(values: Mapping[str, Any]) -> list[str]:
|
|
67
|
+
"""Chart ``env`` keys whose value still holds the scaffold placeholder (``CHANGE-ME``)."""
|
|
68
|
+
return sorted(
|
|
69
|
+
k for k, v in chart_env(values).items() if isinstance(v, str) and _image.has_placeholder(v)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def effective_auth_policy(settings: DeploySettings, values: Mapping[str, Any]) -> str:
|
|
74
|
+
"""``env.AUTH_POLICY`` from the chart values, else the manifest's policy."""
|
|
75
|
+
raw = str(chart_env(values).get("AUTH_POLICY") or "").strip()
|
|
76
|
+
return normalize_auth_policy(raw or settings.auth_policy, warn=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def app_env(values: Mapping[str, Any]) -> str:
|
|
80
|
+
"""``APP_ENV`` as the pod reads it (the app's dev test is exactly ``dev``)."""
|
|
81
|
+
value = chart_env(values).get("APP_ENV")
|
|
82
|
+
return "" if value is None else str(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class JwtFindings:
|
|
87
|
+
"""What keeps the ``jwt`` policy from verifying tokens; ``strict`` outside dev."""
|
|
88
|
+
|
|
89
|
+
env_name: str = ""
|
|
90
|
+
strict: bool = False
|
|
91
|
+
problems: list[str] = field(default_factory=list)
|
|
92
|
+
# What cannot be told before the Secret is read; mentioned only next to problems.
|
|
93
|
+
unverified: list[str] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
def __bool__(self) -> bool:
|
|
96
|
+
return bool(self.problems)
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def where(self) -> str:
|
|
100
|
+
return f"values-{self.env_name}.yaml (or values.yaml) under env:"
|
|
101
|
+
|
|
102
|
+
def error(self) -> str | None:
|
|
103
|
+
"""The refusal (exit 3) outside dev: the pods would refuse to start."""
|
|
104
|
+
if not (self.problems and self.strict):
|
|
105
|
+
return None
|
|
106
|
+
also = f"\n Also check: {'; '.join(self.unverified)}." if self.unverified else ""
|
|
107
|
+
return (
|
|
108
|
+
f"The jwt auth policy cannot verify tokens in {self.env_name}: "
|
|
109
|
+
f"{'; '.join(self.problems)}.\n Set them in {self.where} (a setting the chart env "
|
|
110
|
+
"lists overrides the Secret, even when empty). Outside APP_ENV=dev the pods refuse "
|
|
111
|
+
f"to start without them.{also}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def warning(self) -> str | None:
|
|
115
|
+
"""The dev warning: the pods start, report Ready and answer every request with 503."""
|
|
116
|
+
if not self.problems or self.strict:
|
|
117
|
+
return None
|
|
118
|
+
return (
|
|
119
|
+
f"jwt: {'; '.join(self.problems)}. The {self.env_name} pods start and report Ready "
|
|
120
|
+
f"but answer every request with 503 until it is set in {self.where}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _setting(env: Mapping[str, Any], key: str, secret_keys: Set[str] | None) -> str | None:
|
|
125
|
+
"""What the pod sees for ``key``: the chart env's value when it lists the key (the
|
|
126
|
+
ConfigMap wins, even when empty), else ``"<secret>"`` when the Secret holds it, else
|
|
127
|
+
``""``. ``None`` when that cannot be known (the Secret was not read)."""
|
|
128
|
+
if key in env:
|
|
129
|
+
value = env.get(key)
|
|
130
|
+
return "" if value is None else str(value).strip()
|
|
131
|
+
if secret_keys is None:
|
|
132
|
+
return None
|
|
133
|
+
return "<secret>" if key in secret_keys else ""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def jwt_findings(
|
|
137
|
+
settings: DeploySettings,
|
|
138
|
+
env_name: str,
|
|
139
|
+
values: Mapping[str, Any],
|
|
140
|
+
secret_keys: Set[str] | None,
|
|
141
|
+
) -> JwtFindings:
|
|
142
|
+
"""What keeps the ``jwt`` policy from verifying tokens in ``env_name`` (empty otherwise).
|
|
143
|
+
|
|
144
|
+
``secret_keys`` are the keys the app Secret holds (or will hold after this
|
|
145
|
+
deploy); ``None`` when unknown (argocd mode never reads the cluster), and a
|
|
146
|
+
setting the chart values do not list is then assumed to come from the
|
|
147
|
+
Secret. Strict outside dev (``env_name`` other than ``dev``, or ``APP_ENV``
|
|
148
|
+
not exactly ``dev``), where the pod would refuse to start.
|
|
149
|
+
"""
|
|
150
|
+
findings = JwtFindings(env_name=env_name)
|
|
151
|
+
if effective_auth_policy(settings, values) != "jwt":
|
|
152
|
+
return findings
|
|
153
|
+
env = chart_env(values)
|
|
154
|
+
findings.strict = not _modes.is_dev_env(env_name) or app_env(values) != "dev"
|
|
155
|
+
algorithms = str(env.get("AUTH_JWT_ALGORITHMS") or _DEFAULT_JWT_ALGORITHMS)
|
|
156
|
+
asymmetric = [
|
|
157
|
+
a.strip().upper()
|
|
158
|
+
for a in algorithms.split(",")
|
|
159
|
+
if a.strip() and a.strip().upper() not in _HMAC and a.strip().lower() != "none"
|
|
160
|
+
]
|
|
161
|
+
if asymmetric:
|
|
162
|
+
sources = [_setting(env, key, secret_keys) for key in JWT_KEY_SOURCES]
|
|
163
|
+
if None in sources and not any(sources):
|
|
164
|
+
unknown = [k for k, v in zip(JWT_KEY_SOURCES, sources, strict=True) if v is None]
|
|
165
|
+
findings.unverified.append(
|
|
166
|
+
"no verification key in the chart values, so "
|
|
167
|
+
+ " or ".join(unknown)
|
|
168
|
+
+ " must come from the Secret"
|
|
169
|
+
)
|
|
170
|
+
if all(value == "" for value in sources):
|
|
171
|
+
findings.problems.append(
|
|
172
|
+
"no verification key (set AUTH_JWT_JWKS_URL to the issuer's JWKS, or "
|
|
173
|
+
"AUTH_JWT_PUBLIC_KEY)"
|
|
174
|
+
)
|
|
175
|
+
elif all(value for value in sources):
|
|
176
|
+
findings.problems.append(
|
|
177
|
+
"both AUTH_JWT_JWKS_URL and AUTH_JWT_PUBLIC_KEY are set (set one)"
|
|
178
|
+
)
|
|
179
|
+
if findings.strict:
|
|
180
|
+
for key in JWT_REQUIRED_OUTSIDE_DEV:
|
|
181
|
+
if _setting(env, key, secret_keys) == "":
|
|
182
|
+
findings.problems.append(f"{key} is empty")
|
|
183
|
+
return findings
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def sslmode(dsn: str) -> str | None:
|
|
187
|
+
"""The ``sslmode`` a libpq connection string (URL or ``key=value`` form) asks for."""
|
|
188
|
+
match = _SSLMODE.search(dsn or "")
|
|
189
|
+
return match.group(1).lower() if match else None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def dsn_without_tls(values: Mapping[str, Any], dsn: str) -> bool:
|
|
193
|
+
"""The DSN does not require TLS (and the chart env sets no ``PGSSLMODE`` that does)."""
|
|
194
|
+
mode = sslmode(dsn)
|
|
195
|
+
if mode is None:
|
|
196
|
+
mode = str(chart_env(values).get("PGSSLMODE") or "").strip().lower() or None
|
|
197
|
+
return mode not in TLS_SSLMODES
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _enabled(values: Mapping[str, Any], section: str) -> bool:
|
|
201
|
+
block = values.get(section)
|
|
202
|
+
value = block.get("enabled") if isinstance(block, Mapping) else None
|
|
203
|
+
return value is True or (
|
|
204
|
+
isinstance(value, str) and value.strip().lower() in ("true", "yes", "1")
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def dsn_keys(settings: DeploySettings, values: Mapping[str, Any]) -> list[str]:
|
|
209
|
+
"""Secret keys that hold an external database's connection string.
|
|
210
|
+
|
|
211
|
+
None when the bundled dev subchart provides the database (the chart builds
|
|
212
|
+
that DSN itself) or the fastapi runtime keeps its checkpoints in memory.
|
|
213
|
+
"""
|
|
214
|
+
if _enabled(values, "postgresql"):
|
|
215
|
+
return []
|
|
216
|
+
if settings.runtime == "langgraph-server":
|
|
217
|
+
return ["DATABASE_URI"]
|
|
218
|
+
if str(chart_env(values).get("CHECKPOINTER") or "postgres").strip().lower() == "memory":
|
|
219
|
+
return []
|
|
220
|
+
return ["POSTGRES_DSN"]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def dsn_tls_warning(key: str) -> str:
|
|
224
|
+
return (
|
|
225
|
+
f"{key} does not require TLS (no sslmode=require, verify-ca or verify-full): the "
|
|
226
|
+
"database password and every checkpoint cross the network unencrypted. Append "
|
|
227
|
+
"?sslmode=verify-full&sslrootcert=<CA file mounted into the pod> (or "
|
|
228
|
+
"sslrootcert=system for a publicly trusted certificate), or set PGSSLMODE in the "
|
|
229
|
+
"chart env."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# Peers (protocol: a2a APIs): the rules the runtime applies when it calls them
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
# A credential-carrying call to another agent may use plain http outside APP_ENV=dev only to
|
|
238
|
+
# these hosts: the runtime's `internal_host` (app_utils/api_client.py; a test keeps the two
|
|
239
|
+
# in step).
|
|
240
|
+
INTERNAL_SUFFIXES = (".svc", ".svc.cluster.local")
|
|
241
|
+
CREDENTIAL_AUTH_MODES = ("bearer", "forward", "exchange")
|
|
242
|
+
TOKEN_EXCHANGE_URL = "TOKEN_EXCHANGE_URL"
|
|
243
|
+
TOKEN_EXCHANGE_CLIENT_ID = "TOKEN_EXCHANGE_CLIENT_ID"
|
|
244
|
+
TOKEN_EXCHANGE_CLIENT_SECRET = "TOKEN_EXCHANGE_CLIENT_SECRET"
|
|
245
|
+
PRINCIPAL_HASH_SALT = "PRINCIPAL_HASH_SALT"
|
|
246
|
+
_TRUE = ("1", "true", "yes", "on")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def internal_host(host: str) -> bool:
|
|
250
|
+
"""Whether `host` is loopback, a single-label name or a cluster-internal (`.svc`) name."""
|
|
251
|
+
host = host.strip("[]").lower().rstrip(".")
|
|
252
|
+
if host == "localhost" or ("." not in host and ":" not in host):
|
|
253
|
+
return True
|
|
254
|
+
if host.endswith(INTERNAL_SUFFIXES):
|
|
255
|
+
return True
|
|
256
|
+
try:
|
|
257
|
+
return ipaddress.ip_address(host).is_loopback
|
|
258
|
+
except ValueError:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _loopback(host: str) -> bool:
|
|
263
|
+
host = host.strip("[]").lower()
|
|
264
|
+
if host == "localhost":
|
|
265
|
+
return True
|
|
266
|
+
try:
|
|
267
|
+
return ipaddress.ip_address(host).is_loopback
|
|
268
|
+
except ValueError:
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass
|
|
273
|
+
class PeerFindings:
|
|
274
|
+
"""What keeps the agent from calling its peers in an environment (``strict`` outside dev:
|
|
275
|
+
the runtime refuses those calls, or refuses to start)."""
|
|
276
|
+
|
|
277
|
+
env_name: str = ""
|
|
278
|
+
strict: bool = False
|
|
279
|
+
problems: list[str] = field(default_factory=list)
|
|
280
|
+
warnings: list[str] = field(default_factory=list)
|
|
281
|
+
|
|
282
|
+
def error(self) -> str | None:
|
|
283
|
+
"""The refusal (exit 3) outside dev."""
|
|
284
|
+
if not (self.problems and self.strict):
|
|
285
|
+
return None
|
|
286
|
+
lines = "\n".join(f" - {problem}" for problem in self.problems)
|
|
287
|
+
return (
|
|
288
|
+
f"The agent could not call the agents in api-policy.yaml in {self.env_name}:\n{lines}"
|
|
289
|
+
f"\n Set them in values-{self.env_name}.yaml (or values.yaml) under env:."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def notes(self) -> list[str]:
|
|
293
|
+
"""The warnings, and in dev the problems too (they warn there)."""
|
|
294
|
+
return [*([] if self.strict else self.problems), *self.warnings]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def peer_findings(
|
|
298
|
+
settings: DeploySettings,
|
|
299
|
+
env_name: str,
|
|
300
|
+
values: Mapping[str, Any],
|
|
301
|
+
document: Mapping[str, Any] | None,
|
|
302
|
+
) -> PeerFindings:
|
|
303
|
+
"""The peer rules (``deploy``'s pre-checks; design 7.4) for ``env_name``.
|
|
304
|
+
|
|
305
|
+
Refused outside dev: a credential-carrying peer URL over http to a host that
|
|
306
|
+
is not internal (loopback, a single label, ``.svc``); an ``auth: exchange`` API
|
|
307
|
+
without ``TOKEN_EXCHANGE_URL`` or ``TOKEN_EXCHANGE_CLIENT_ID``, or with an http
|
|
308
|
+
token URL to a host that is not loopback (unless ``TOKEN_EXCHANGE_ALLOW_HTTP``).
|
|
309
|
+
A peer URL still at ``CHANGE-ME`` is refused by ``env_placeholders`` already.
|
|
310
|
+
Warned: an unset peer URL, and what ``secrets.keys`` lacks (the exchange
|
|
311
|
+
client secret, ``PRINCIPAL_HASH_SALT``).
|
|
312
|
+
"""
|
|
313
|
+
findings = PeerFindings(env_name=env_name)
|
|
314
|
+
findings.strict = not _modes.is_dev_env(env_name) or app_env(values) != "dev"
|
|
315
|
+
apis = (document or {}).get("apis") or {}
|
|
316
|
+
peers = {
|
|
317
|
+
str(n): a for n, a in apis.items() if isinstance(a, Mapping) and a.get("protocol") == "a2a"
|
|
318
|
+
}
|
|
319
|
+
env = chart_env(values)
|
|
320
|
+
for name, api in peers.items():
|
|
321
|
+
variable = str(api.get("base_url_env") or "")
|
|
322
|
+
url = str(env.get(variable) or "").strip()
|
|
323
|
+
if not url:
|
|
324
|
+
findings.warnings.append(
|
|
325
|
+
f"{variable} (the URL of the agent behind {name}) is not in the chart env: set it "
|
|
326
|
+
f"in values-{env_name}.yaml, or the calls to {name} fail"
|
|
327
|
+
)
|
|
328
|
+
continue
|
|
329
|
+
if _image.has_placeholder(url) or api.get("auth") not in CREDENTIAL_AUTH_MODES:
|
|
330
|
+
continue
|
|
331
|
+
parts = urllib.parse.urlsplit(url)
|
|
332
|
+
if parts.scheme == "http" and not internal_host(parts.hostname or ""):
|
|
333
|
+
findings.problems.append(
|
|
334
|
+
f"{variable} ({url}) must use https outside APP_ENV=dev to carry credentials "
|
|
335
|
+
f"(auth: {api.get('auth')}); plain http is for loopback and cluster-internal "
|
|
336
|
+
"names only"
|
|
337
|
+
)
|
|
338
|
+
keys = set(settings.secret_keys)
|
|
339
|
+
if any(api.get("auth") == "exchange" for api in apis.values() if isinstance(api, Mapping)):
|
|
340
|
+
for variable in (TOKEN_EXCHANGE_URL, TOKEN_EXCHANGE_CLIENT_ID):
|
|
341
|
+
if not str(env.get(variable) or "").strip():
|
|
342
|
+
findings.problems.append(
|
|
343
|
+
f"{variable} is not set, and auth: exchange APIs need it (the issuer's token "
|
|
344
|
+
"endpoint and this agent's client there)"
|
|
345
|
+
)
|
|
346
|
+
token = str(env.get(TOKEN_EXCHANGE_URL) or "").strip()
|
|
347
|
+
allow_http = str(env.get("TOKEN_EXCHANGE_ALLOW_HTTP") or "").strip().lower() in _TRUE
|
|
348
|
+
parts = urllib.parse.urlsplit(token)
|
|
349
|
+
if (
|
|
350
|
+
token
|
|
351
|
+
and not _image.has_placeholder(token)
|
|
352
|
+
and parts.scheme == "http"
|
|
353
|
+
and not (_loopback(parts.hostname or "") or allow_http)
|
|
354
|
+
):
|
|
355
|
+
findings.problems.append(
|
|
356
|
+
f"{TOKEN_EXCHANGE_URL} ({token}) must use https outside APP_ENV=dev (it carries "
|
|
357
|
+
'the users\' tokens and the client secret); set TOKEN_EXCHANGE_ALLOW_HTTP: "true" '
|
|
358
|
+
"only for a trusted in-cluster issuer"
|
|
359
|
+
)
|
|
360
|
+
if TOKEN_EXCHANGE_CLIENT_SECRET not in keys:
|
|
361
|
+
findings.warnings.append(
|
|
362
|
+
f"{TOKEN_EXCHANGE_CLIENT_SECRET} is not in secrets.keys: the pods never get it, so "
|
|
363
|
+
"every token exchange fails"
|
|
364
|
+
)
|
|
365
|
+
if peers and PRINCIPAL_HASH_SALT not in keys:
|
|
366
|
+
findings.warnings.append(
|
|
367
|
+
f"{PRINCIPAL_HASH_SALT} is not in secrets.keys: the conversation ids sent to other "
|
|
368
|
+
"agents are keyed without a secret"
|
|
369
|
+
)
|
|
370
|
+
return findings
|