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,658 @@
|
|
|
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
|
+
"""The files an ``api`` command changes: planned as text, shown as a diff, written atomically.
|
|
16
|
+
|
|
17
|
+
Besides ``api-policy.yaml`` a change keeps the rest of the project in step:
|
|
18
|
+
the manifest (``api_policy.policy_file``, and in ``secrets.keys`` the bearer
|
|
19
|
+
``token_env`` and, for ``auth: exchange``, ``TOKEN_EXCHANGE_CLIENT_SECRET``),
|
|
20
|
+
``.env.example`` and the chart's ``values.yaml`` ``env`` map (each API's
|
|
21
|
+
``base_url_env``; for ``auth: exchange``, ``TOKEN_EXCHANGE_URL`` and
|
|
22
|
+
``TOKEN_EXCHANGE_CLIENT_ID`` once). Every edit keeps comments and formatting
|
|
23
|
+
(:mod:`graph_agents_cli.scaffold.utils.keyedit`); one that cannot be made
|
|
24
|
+
safely becomes a "Left for you" item instead.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import difflib
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import tempfile
|
|
33
|
+
from collections.abc import Mapping
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
import click
|
|
39
|
+
|
|
40
|
+
from graph_agents_cli._api_policy import (
|
|
41
|
+
POLICY_FILENAME,
|
|
42
|
+
TOKEN_EXCHANGE_CLIENT_ID_ENV,
|
|
43
|
+
TOKEN_EXCHANGE_URL_ENV,
|
|
44
|
+
secret_envs,
|
|
45
|
+
summarize,
|
|
46
|
+
)
|
|
47
|
+
from graph_agents_cli._defaults import default_secret_keys
|
|
48
|
+
from graph_agents_cli._project import MANIFEST_FILENAME, ProjectConfig
|
|
49
|
+
from graph_agents_cli.scaffold.utils.keyedit import (
|
|
50
|
+
EditError,
|
|
51
|
+
YamlText,
|
|
52
|
+
env_insert,
|
|
53
|
+
env_names,
|
|
54
|
+
env_remove,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
ENV_EXAMPLE = ".env.example"
|
|
58
|
+
ENV_SECTION = re.compile(r"^# --- Outbound APIs")
|
|
59
|
+
ENV_BEFORE = re.compile(r"^# Path of the policy file")
|
|
60
|
+
# The note a project without a policy carries in .env.example (older and current wording).
|
|
61
|
+
ENV_NO_POLICY_NOTES = [
|
|
62
|
+
"# No api-policy.yaml is declared, so every outbound API call is refused. Declare",
|
|
63
|
+
"# the APIs tools may call in api-policy.yaml (graph-agents-cli create --api-policy).",
|
|
64
|
+
"# the APIs tools may call with `graph-agents-cli api add` (see README.md).",
|
|
65
|
+
]
|
|
66
|
+
# The current wording, as a fresh project without a policy has it.
|
|
67
|
+
ENV_NO_POLICY_NOTE = [ENV_NO_POLICY_NOTES[0], ENV_NO_POLICY_NOTES[2]]
|
|
68
|
+
# The line above an API's variables (written by `create`, `api add` and `peer add`, which
|
|
69
|
+
# names the peer: `# orders_agent (A2A peer orders; auth: exchange)`).
|
|
70
|
+
ENV_API_HEADER = re.compile(
|
|
71
|
+
r"^# ([a-z][a-z0-9_]{0,31}) \((?:A2A peer [a-z][a-z0-9_]{0,31}; )?auth: [a-z]+\)$"
|
|
72
|
+
)
|
|
73
|
+
LOCAL_BASE_URL = "http://localhost:9000"
|
|
74
|
+
CHART_BASE_URL = "http://CHANGE-ME"
|
|
75
|
+
VALUES_COMMENT = "Base URLs of the APIs in api-policy.yaml (tokens come from the Secret)."
|
|
76
|
+
# `auth: exchange`: the issuer's token endpoint and this agent's client there, once for every
|
|
77
|
+
# exchange API. `.env.example` lists them under this note (the secret commented out: it goes
|
|
78
|
+
# in .env and reaches the Secret through secrets.keys); `create` renders the same lines, with
|
|
79
|
+
# the optional settings (`EXCHANGE_ENV_OPTIONAL`) between the note and the secret.
|
|
80
|
+
EXCHANGE_ENV_NOTE = [
|
|
81
|
+
"# Token exchange (RFC 8693) for the auth: exchange APIs: the issuer's token endpoint",
|
|
82
|
+
"# and this agent's client there. Put the client secret in .env (it is in secrets.keys).",
|
|
83
|
+
]
|
|
84
|
+
EXCHANGE_ENV_OPTIONAL = [
|
|
85
|
+
"# Optional: client authentication (or client_secret_post), the subject token type",
|
|
86
|
+
"# sent, the exchange's deadline, how long an exchanged token is reused (at most",
|
|
87
|
+
"# 300 s), how long a refusal or a failing issuer is left alone, tokens kept per",
|
|
88
|
+
"# process, and a plain-http token URL outside dev (a trusted in-cluster issuer only).",
|
|
89
|
+
"# TOKEN_EXCHANGE_CLIENT_AUTH=client_secret_basic",
|
|
90
|
+
"# TOKEN_EXCHANGE_SUBJECT_TOKEN_TYPE=urn:ietf:params:oauth:token-type:access_token",
|
|
91
|
+
"# TOKEN_EXCHANGE_TIMEOUT_MS=2000",
|
|
92
|
+
"# TOKEN_EXCHANGE_MAX_TTL_S=300",
|
|
93
|
+
"# TOKEN_EXCHANGE_FAILURE_TTL_S=10",
|
|
94
|
+
"# TOKEN_EXCHANGE_CACHE_MAX=10000",
|
|
95
|
+
"# TOKEN_EXCHANGE_ALLOW_HTTP=false",
|
|
96
|
+
]
|
|
97
|
+
EXCHANGE_ENV_SECRET = "# TOKEN_EXCHANGE_CLIENT_SECRET="
|
|
98
|
+
EXCHANGE_VARIABLES = (TOKEN_EXCHANGE_URL_ENV, TOKEN_EXCHANGE_CLIENT_ID_ENV)
|
|
99
|
+
EXCHANGE_VALUES_COMMENT = (
|
|
100
|
+
"Token exchange (RFC 8693) for the auth: exchange APIs (the client secret comes from the "
|
|
101
|
+
"Secret)."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class FileChange:
|
|
107
|
+
"""One file's text before and after (None: the file does not exist).
|
|
108
|
+
|
|
109
|
+
``summary`` replaces the file's diff in the output (a copied spec is
|
|
110
|
+
hundreds of lines nobody needs to review line by line here).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
path: str # relative to the project root
|
|
114
|
+
before: str | None
|
|
115
|
+
after: str | None
|
|
116
|
+
summary: str | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class Plan:
|
|
121
|
+
"""Every file a command changes, plus what it could not do and what to know."""
|
|
122
|
+
|
|
123
|
+
root: Path
|
|
124
|
+
changes: list[FileChange] = field(default_factory=list)
|
|
125
|
+
left_for_you: list[str] = field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
def set_text(
|
|
128
|
+
self, path: str, before: str | None, after: str | None, *, summary: str | None = None
|
|
129
|
+
) -> None:
|
|
130
|
+
for change in self.changes:
|
|
131
|
+
if change.path == path:
|
|
132
|
+
change.after = after
|
|
133
|
+
change.summary = summary or change.summary
|
|
134
|
+
return
|
|
135
|
+
if before != after:
|
|
136
|
+
self.changes.append(FileChange(path, before, after, summary))
|
|
137
|
+
|
|
138
|
+
def current(self, path: str) -> str | None:
|
|
139
|
+
"""``path``'s text after the changes planned so far (its stored text when none is).
|
|
140
|
+
|
|
141
|
+
Every edit starts from this text, so several edits of one file in one plan
|
|
142
|
+
(``system apply`` adds many peers at once) build on each other.
|
|
143
|
+
"""
|
|
144
|
+
for change in self.changes:
|
|
145
|
+
if change.path == path:
|
|
146
|
+
return change.after
|
|
147
|
+
return read_text(self.root / path)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def effective(self) -> list[FileChange]:
|
|
151
|
+
return [c for c in self.changes if c.before != c.after]
|
|
152
|
+
|
|
153
|
+
def summaries(self) -> list[str]:
|
|
154
|
+
"""One line per summarised change (shown after the diff of the others)."""
|
|
155
|
+
return [f"{c.path}: {c.summary}" for c in self.effective if c.summary]
|
|
156
|
+
|
|
157
|
+
def diff(self) -> str:
|
|
158
|
+
"""The unified diff of every change that is not summarised."""
|
|
159
|
+
chunks = []
|
|
160
|
+
for change in self.effective:
|
|
161
|
+
if change.summary:
|
|
162
|
+
continue
|
|
163
|
+
before = (change.before or "").splitlines(keepends=True)
|
|
164
|
+
after = (change.after or "").splitlines(keepends=True)
|
|
165
|
+
chunks.extend(
|
|
166
|
+
_terminated(
|
|
167
|
+
difflib.unified_diff(
|
|
168
|
+
before,
|
|
169
|
+
after,
|
|
170
|
+
fromfile="/dev/null" if change.before is None else f"a/{change.path}",
|
|
171
|
+
tofile="/dev/null" if change.after is None else f"b/{change.path}",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return "".join(chunks)
|
|
176
|
+
|
|
177
|
+
def write(self) -> None:
|
|
178
|
+
"""Write every change atomically (a temporary file renamed over the old one).
|
|
179
|
+
|
|
180
|
+
An existing file keeps its mode. A new one gets the mode any new file
|
|
181
|
+
gets (0666 less the umask), not the 0600 of the temporary file: the
|
|
182
|
+
images copy the project's files as they are and run as uid 1000, so a
|
|
183
|
+
private api-policy.yaml would be unreadable there and the agent would
|
|
184
|
+
refuse every call.
|
|
185
|
+
"""
|
|
186
|
+
for change in self.effective:
|
|
187
|
+
target = self.root / change.path
|
|
188
|
+
if change.after is None:
|
|
189
|
+
target.unlink(missing_ok=True)
|
|
190
|
+
continue
|
|
191
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
mode = target.stat().st_mode & 0o777 if target.exists() else _new_file_mode()
|
|
193
|
+
handle, temporary = tempfile.mkstemp(
|
|
194
|
+
prefix=f".{target.name}.", suffix=".tmp", dir=target.parent
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
with os.fdopen(handle, "w", encoding="utf-8", newline="") as stream:
|
|
198
|
+
stream.write(change.after)
|
|
199
|
+
os.chmod(temporary, mode)
|
|
200
|
+
os.replace(temporary, target)
|
|
201
|
+
except BaseException:
|
|
202
|
+
Path(temporary).unlink(missing_ok=True)
|
|
203
|
+
raise
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _new_file_mode() -> int:
|
|
207
|
+
"""The mode ``open()`` gives a new file: 0666 less the process umask."""
|
|
208
|
+
umask = os.umask(0o022) # reading the umask means setting it; restored at once
|
|
209
|
+
os.umask(umask)
|
|
210
|
+
return 0o666 & ~umask
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _terminated(lines: Any) -> list[str]:
|
|
214
|
+
out = []
|
|
215
|
+
for line in lines:
|
|
216
|
+
out.append(line if line.endswith("\n") else line + "\n\\n")
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def print_diff(text: str) -> None:
|
|
221
|
+
for line in text.splitlines():
|
|
222
|
+
if line.startswith(("+++", "---")):
|
|
223
|
+
click.secho(line, bold=True)
|
|
224
|
+
elif line.startswith("+"):
|
|
225
|
+
click.secho(line, fg="green")
|
|
226
|
+
elif line.startswith("-"):
|
|
227
|
+
click.secho(line, fg="red")
|
|
228
|
+
elif line.startswith("@@"):
|
|
229
|
+
click.secho(line, fg="cyan")
|
|
230
|
+
else:
|
|
231
|
+
click.echo(line)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def read_text(path: Path) -> str | None:
|
|
235
|
+
"""The file's text as stored: line endings are kept (a CRLF file stays CRLF)."""
|
|
236
|
+
try:
|
|
237
|
+
with path.open(encoding="utf-8", newline="") as stream:
|
|
238
|
+
return stream.read()
|
|
239
|
+
except FileNotFoundError:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# The manifest
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def sync_manifest(
|
|
249
|
+
plan: Plan,
|
|
250
|
+
config: ProjectConfig,
|
|
251
|
+
*,
|
|
252
|
+
document: dict[str, Any] | None,
|
|
253
|
+
previous: dict[str, Any] | None,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""``api_policy`` present exactly when there is a policy; bearer tokens in ``secrets.keys``.
|
|
256
|
+
|
|
257
|
+
A token variable goes when the last API using it goes, unless it is one of
|
|
258
|
+
the project's default keys (the provider key, API_KEY, ...).
|
|
259
|
+
"""
|
|
260
|
+
stored = read_text(plan.root / MANIFEST_FILENAME)
|
|
261
|
+
before = plan.current(MANIFEST_FILENAME)
|
|
262
|
+
if before is None:
|
|
263
|
+
plan.left_for_you.append(f"{MANIFEST_FILENAME} not found; nothing recorded there")
|
|
264
|
+
return
|
|
265
|
+
try:
|
|
266
|
+
manifest = YamlText(before)
|
|
267
|
+
except EditError as exc:
|
|
268
|
+
plan.left_for_you.append(f"{MANIFEST_FILENAME} could not be edited safely ({exc})")
|
|
269
|
+
return
|
|
270
|
+
new_tokens = secret_envs(summarize(document)) if document else []
|
|
271
|
+
old_tokens = secret_envs(summarize(previous)) if previous else []
|
|
272
|
+
defaults = default_secret_keys(
|
|
273
|
+
config.model_provider, config.runtime, auth_policy=config.auth_policy
|
|
274
|
+
)
|
|
275
|
+
steps: list[tuple[str, Any]] = []
|
|
276
|
+
if document is not None:
|
|
277
|
+
if manifest.get(("api_policy", "policy_file")) != POLICY_FILENAME:
|
|
278
|
+
steps.append(("policy", {"policy_file": POLICY_FILENAME}))
|
|
279
|
+
elif manifest.get(("api_policy",)) is not None:
|
|
280
|
+
steps.append(("no-policy", None))
|
|
281
|
+
keys = manifest.get(("secrets", "keys"))
|
|
282
|
+
if isinstance(keys, list):
|
|
283
|
+
for token in new_tokens:
|
|
284
|
+
if token not in keys:
|
|
285
|
+
steps.append(("add-key", token))
|
|
286
|
+
for token in old_tokens:
|
|
287
|
+
if token not in new_tokens and token not in defaults and token in keys:
|
|
288
|
+
steps.append(("remove-key", token))
|
|
289
|
+
elif new_tokens:
|
|
290
|
+
steps.append(("keys", [*defaults, *(t for t in new_tokens if t not in defaults)]))
|
|
291
|
+
for kind, value in steps:
|
|
292
|
+
try:
|
|
293
|
+
if kind == "policy":
|
|
294
|
+
manifest.set(("api_policy",), value, after="secrets", block=True)
|
|
295
|
+
elif kind == "no-policy":
|
|
296
|
+
manifest.delete(("api_policy",))
|
|
297
|
+
elif kind == "add-key":
|
|
298
|
+
manifest.append(("secrets", "keys"), value)
|
|
299
|
+
elif kind == "remove-key":
|
|
300
|
+
current = manifest.get(("secrets", "keys"))
|
|
301
|
+
manifest.remove_item(("secrets", "keys"), current.index(value))
|
|
302
|
+
else:
|
|
303
|
+
manifest.set(("secrets", "keys"), value)
|
|
304
|
+
except EditError as exc:
|
|
305
|
+
plan.left_for_you.append(_manifest_todo(kind, value, exc))
|
|
306
|
+
plan.set_text(MANIFEST_FILENAME, stored, manifest.text)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _manifest_todo(kind: str, value: Any, exc: EditError) -> str:
|
|
310
|
+
what = {
|
|
311
|
+
"policy": f"set api_policy: {{policy_file: {POLICY_FILENAME}}}",
|
|
312
|
+
"no-policy": "remove the api_policy block",
|
|
313
|
+
"add-key": f"add {value} to secrets.keys",
|
|
314
|
+
"remove-key": f"remove {value} from secrets.keys",
|
|
315
|
+
"keys": f"set secrets.keys to {value}",
|
|
316
|
+
}[kind]
|
|
317
|
+
return f"{MANIFEST_FILENAME}: {what} (not edited automatically: {exc})"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# .env.example
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _env_comment(name: str, api: dict[str, Any], peer: str | None = None) -> str:
|
|
326
|
+
if peer is not None:
|
|
327
|
+
return f"# {name} (A2A peer {peer}; auth: {api['auth']})"
|
|
328
|
+
return f"# {name} (auth: {api['auth']})"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def env_example_add(
|
|
332
|
+
plan: Plan,
|
|
333
|
+
name: str,
|
|
334
|
+
api: dict[str, Any],
|
|
335
|
+
*,
|
|
336
|
+
peer: str | None = None,
|
|
337
|
+
base_url: str = LOCAL_BASE_URL,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Document an API's variables in ``.env.example`` (``peer``: the A2A peer it is)."""
|
|
340
|
+
stored = read_text(plan.root / ENV_EXAMPLE)
|
|
341
|
+
before = plan.current(ENV_EXAMPLE)
|
|
342
|
+
if before is None:
|
|
343
|
+
return
|
|
344
|
+
variables = [(api["base_url_env"], base_url)]
|
|
345
|
+
if api["auth"] == "bearer":
|
|
346
|
+
variables.append((api["token_env"], ""))
|
|
347
|
+
exchange = api["auth"] == "exchange"
|
|
348
|
+
try:
|
|
349
|
+
present = env_names(before)
|
|
350
|
+
missing = [(var, value) for var, value in variables if var not in present]
|
|
351
|
+
after = before
|
|
352
|
+
if missing:
|
|
353
|
+
lines = [
|
|
354
|
+
_env_comment(name, api, peer),
|
|
355
|
+
*(f"{var}={value}" for var, value in missing),
|
|
356
|
+
]
|
|
357
|
+
after = env_insert(
|
|
358
|
+
after, lines, section=ENV_SECTION, before=ENV_BEFORE, drop=ENV_NO_POLICY_NOTES
|
|
359
|
+
)
|
|
360
|
+
if exchange and not set(EXCHANGE_VARIABLES) & present:
|
|
361
|
+
lines = [
|
|
362
|
+
*EXCHANGE_ENV_NOTE,
|
|
363
|
+
EXCHANGE_ENV_SECRET,
|
|
364
|
+
*(f"{v}=" for v in EXCHANGE_VARIABLES),
|
|
365
|
+
]
|
|
366
|
+
after = env_insert(after, lines, section=ENV_SECTION, before=ENV_BEFORE)
|
|
367
|
+
except (EditError, ValueError) as exc:
|
|
368
|
+
wanted = [var for var, _ in variables] + (list(EXCHANGE_VARIABLES) if exchange else [])
|
|
369
|
+
plan.left_for_you.append(f"{ENV_EXAMPLE}: document {', '.join(wanted)} (not edited: {exc})")
|
|
370
|
+
return
|
|
371
|
+
plan.set_text(ENV_EXAMPLE, stored, after)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _api_variables(api: Mapping[str, Any]) -> set[str]:
|
|
375
|
+
"""The variables an API puts in ``.env.example``: its base URL, and its token under bearer."""
|
|
376
|
+
names = {str(api["base_url_env"])}
|
|
377
|
+
if api["auth"] == "bearer":
|
|
378
|
+
names.add(str(api["token_env"]))
|
|
379
|
+
if api["auth"] == "exchange":
|
|
380
|
+
names.update(EXCHANGE_VARIABLES)
|
|
381
|
+
return names
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def env_example_remove(
|
|
385
|
+
plan: Plan,
|
|
386
|
+
name: str,
|
|
387
|
+
api: dict[str, Any],
|
|
388
|
+
keep: set[str],
|
|
389
|
+
remaining: Mapping[str, Mapping[str, Any]],
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Drop the variables of API ``name`` that no other API uses (``keep``).
|
|
392
|
+
|
|
393
|
+
``remaining``: the APIs left, by name. The header line of an API that is
|
|
394
|
+
gone goes once none of its variables remain; when a variable another API
|
|
395
|
+
shares keeps it, it is renamed to that API. Without any API left the
|
|
396
|
+
fresh project's "no policy" note comes back.
|
|
397
|
+
"""
|
|
398
|
+
stored = read_text(plan.root / ENV_EXAMPLE)
|
|
399
|
+
before = plan.current(ENV_EXAMPLE)
|
|
400
|
+
if before is None:
|
|
401
|
+
return
|
|
402
|
+
names = [api["base_url_env"]] + ([api["token_env"]] if api["auth"] == "bearer" else [])
|
|
403
|
+
names = [n for n in names if n not in keep]
|
|
404
|
+
no_exchange = not any(other.get("auth") == "exchange" for other in remaining.values())
|
|
405
|
+
try:
|
|
406
|
+
names = [n for n in names if n in env_names(before)]
|
|
407
|
+
after = before
|
|
408
|
+
if names:
|
|
409
|
+
headers = [_env_comment(name, api), *_peer_comments(before, name)]
|
|
410
|
+
after = env_remove(before, names, comments=headers)
|
|
411
|
+
exchange_names = [n for n in EXCHANGE_VARIABLES if n in env_names(after)]
|
|
412
|
+
if no_exchange and exchange_names:
|
|
413
|
+
after = env_remove(
|
|
414
|
+
after,
|
|
415
|
+
exchange_names,
|
|
416
|
+
comments=[*EXCHANGE_ENV_NOTE, *EXCHANGE_ENV_OPTIONAL, EXCHANGE_ENV_SECRET],
|
|
417
|
+
)
|
|
418
|
+
after = _fix_orphan_headers(after, remaining)
|
|
419
|
+
if not remaining:
|
|
420
|
+
after = _restore_no_policy_note(after)
|
|
421
|
+
except (EditError, ValueError) as exc:
|
|
422
|
+
plan.left_for_you.append(f"{ENV_EXAMPLE}: remove {', '.join(names)} (not edited: {exc})")
|
|
423
|
+
return
|
|
424
|
+
plan.set_text(ENV_EXAMPLE, stored, after)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _peer_comments(text: str, name: str) -> list[str]:
|
|
428
|
+
"""The header lines ``peer add`` wrote for API ``name`` (whichever peer and auth)."""
|
|
429
|
+
found = []
|
|
430
|
+
for line in text.splitlines():
|
|
431
|
+
stripped = line.strip()
|
|
432
|
+
match = ENV_API_HEADER.match(stripped)
|
|
433
|
+
if match and match.group(1) == name and "(A2A peer " in stripped:
|
|
434
|
+
found.append(stripped)
|
|
435
|
+
return found
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _fix_orphan_headers(text: str, remaining: Mapping[str, Mapping[str, Any]]) -> str:
|
|
439
|
+
"""``text`` with the headers of APIs that are gone dropped or handed on.
|
|
440
|
+
|
|
441
|
+
A header that no longer heads a variable goes. One that still heads a
|
|
442
|
+
variable a remaining API uses names that API (the first in the policy)
|
|
443
|
+
instead, so the file never credits a variable to an API that is gone.
|
|
444
|
+
"""
|
|
445
|
+
lines = text.splitlines(keepends=True)
|
|
446
|
+
kept = []
|
|
447
|
+
for index, line in enumerate(lines):
|
|
448
|
+
stripped = line.rstrip("\r\n")
|
|
449
|
+
match = ENV_API_HEADER.match(stripped)
|
|
450
|
+
if match and match.group(1) not in remaining:
|
|
451
|
+
headed = _headed_variables(lines, index)
|
|
452
|
+
if not headed:
|
|
453
|
+
continue
|
|
454
|
+
owner = next((n for n, a in remaining.items() if headed & _api_variables(a)), None)
|
|
455
|
+
if owner is not None:
|
|
456
|
+
line = _env_comment(owner, dict(remaining[owner])) + line[len(stripped) :]
|
|
457
|
+
kept.append(line)
|
|
458
|
+
return "".join(kept)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _headed_variables(lines: list[str], index: int) -> set[str]:
|
|
462
|
+
"""The variables that follow line ``index`` before a blank line or another header."""
|
|
463
|
+
names: set[str] = set()
|
|
464
|
+
for line in lines[index + 1 :]:
|
|
465
|
+
stripped = line.strip()
|
|
466
|
+
if not stripped or stripped.startswith("# ---") or ENV_API_HEADER.match(stripped):
|
|
467
|
+
break
|
|
468
|
+
names |= env_names(line)
|
|
469
|
+
return names
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _restore_no_policy_note(text: str) -> str:
|
|
473
|
+
"""The Outbound APIs section says again that no policy is declared (when it has none)."""
|
|
474
|
+
if not any(ENV_SECTION.match(line) for line in text.splitlines()):
|
|
475
|
+
return text
|
|
476
|
+
present = {line.strip() for line in text.splitlines()}
|
|
477
|
+
if ENV_NO_POLICY_NOTE[0] in present:
|
|
478
|
+
return text
|
|
479
|
+
return env_insert(text, ENV_NO_POLICY_NOTE, section=ENV_SECTION, before=ENV_BEFORE)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# ---------------------------------------------------------------------------
|
|
483
|
+
# The chart's values
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def chart_values_files(root: Path, config: ProjectConfig) -> list[Path]:
|
|
488
|
+
"""``values.yaml`` then ``values-<env>.yaml`` of the project's chart (empty without one)."""
|
|
489
|
+
if config.deployment_target != "kubernetes":
|
|
490
|
+
return []
|
|
491
|
+
chart = root / "deployment" / "helm" / (config.project_name or "")
|
|
492
|
+
if not (chart / "values.yaml").is_file():
|
|
493
|
+
return []
|
|
494
|
+
return [chart / "values.yaml", *sorted(chart.glob("values-*.yaml"))]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def values_add(plan: Plan, config: ProjectConfig, document: dict[str, Any], name: str) -> None:
|
|
498
|
+
files = chart_values_files(plan.root, config)
|
|
499
|
+
if not files:
|
|
500
|
+
return
|
|
501
|
+
path = files[0]
|
|
502
|
+
rel = path.relative_to(plan.root).as_posix()
|
|
503
|
+
before = plan.current(rel)
|
|
504
|
+
api = document["apis"][name]
|
|
505
|
+
variable = api["base_url_env"]
|
|
506
|
+
try:
|
|
507
|
+
values = YamlText(before or "")
|
|
508
|
+
env = values.get(("env",))
|
|
509
|
+
if not isinstance(env, dict):
|
|
510
|
+
raise EditError("values.yaml has no env: mapping")
|
|
511
|
+
if variable in env:
|
|
512
|
+
return
|
|
513
|
+
others = [
|
|
514
|
+
str(other["base_url_env"])
|
|
515
|
+
for other_name, other in document["apis"].items()
|
|
516
|
+
if other_name != name and str(other["base_url_env"]) in env
|
|
517
|
+
]
|
|
518
|
+
if others:
|
|
519
|
+
values.set(("env", variable), CHART_BASE_URL, after=others[-1])
|
|
520
|
+
else:
|
|
521
|
+
comment = None if VALUES_COMMENT in (before or "") else VALUES_COMMENT
|
|
522
|
+
values.set(("env", variable), CHART_BASE_URL, after="AUTH_ADMIN_ROLES", comment=comment)
|
|
523
|
+
except EditError as exc:
|
|
524
|
+
plan.left_for_you.append(
|
|
525
|
+
f"{rel}: add {variable}: <the API's base URL> under env: (not edited: {exc})"
|
|
526
|
+
)
|
|
527
|
+
return
|
|
528
|
+
plan.set_text(rel, read_text(path), values.text)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def values_exchange_add(
|
|
532
|
+
plan: Plan, config: ProjectConfig, document: dict[str, Any], name: str
|
|
533
|
+
) -> None:
|
|
534
|
+
"""``TOKEN_EXCHANGE_URL`` (a placeholder) and ``TOKEN_EXCHANGE_CLIENT_ID`` (the project's
|
|
535
|
+
name) in the chart's ``values.yaml`` ``env``, once, after the new exchange API's base URL."""
|
|
536
|
+
files = chart_values_files(plan.root, config)
|
|
537
|
+
if not files or document["apis"][name]["auth"] != "exchange":
|
|
538
|
+
return
|
|
539
|
+
path = files[0]
|
|
540
|
+
rel = path.relative_to(plan.root).as_posix()
|
|
541
|
+
before = plan.current(rel)
|
|
542
|
+
try:
|
|
543
|
+
values = YamlText(before or "")
|
|
544
|
+
env = values.get(("env",))
|
|
545
|
+
if not isinstance(env, dict):
|
|
546
|
+
raise EditError("values.yaml has no env: mapping")
|
|
547
|
+
if TOKEN_EXCHANGE_URL_ENV in env and TOKEN_EXCHANGE_CLIENT_ID_ENV in env:
|
|
548
|
+
return
|
|
549
|
+
after = str(document["apis"][name]["base_url_env"])
|
|
550
|
+
if TOKEN_EXCHANGE_URL_ENV not in env:
|
|
551
|
+
values.set(
|
|
552
|
+
("env", TOKEN_EXCHANGE_URL_ENV),
|
|
553
|
+
CHART_BASE_URL,
|
|
554
|
+
after=after if after in env else None,
|
|
555
|
+
comment=EXCHANGE_VALUES_COMMENT,
|
|
556
|
+
)
|
|
557
|
+
if TOKEN_EXCHANGE_CLIENT_ID_ENV not in values.get(("env",)):
|
|
558
|
+
values.set(
|
|
559
|
+
("env", TOKEN_EXCHANGE_CLIENT_ID_ENV),
|
|
560
|
+
config.project_name or "CHANGE-ME",
|
|
561
|
+
after=TOKEN_EXCHANGE_URL_ENV,
|
|
562
|
+
)
|
|
563
|
+
except EditError as exc:
|
|
564
|
+
plan.left_for_you.append(
|
|
565
|
+
f"{rel}: add {TOKEN_EXCHANGE_URL_ENV}: <the issuer's token endpoint> and "
|
|
566
|
+
f"{TOKEN_EXCHANGE_CLIENT_ID_ENV}: <this agent's client id> under env: (not edited: "
|
|
567
|
+
f"{exc})"
|
|
568
|
+
)
|
|
569
|
+
return
|
|
570
|
+
plan.set_text(rel, read_text(path), values.text)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def values_exchange_remove(plan: Plan, config: ProjectConfig) -> None:
|
|
574
|
+
"""Drop ``TOKEN_EXCHANGE_URL`` and ``TOKEN_EXCHANGE_CLIENT_ID`` from the chart's env maps
|
|
575
|
+
(the last exchange API went), with their comment line."""
|
|
576
|
+
for path in chart_values_files(plan.root, config):
|
|
577
|
+
rel = path.relative_to(plan.root).as_posix()
|
|
578
|
+
stored = read_text(path)
|
|
579
|
+
before = plan.current(rel)
|
|
580
|
+
try:
|
|
581
|
+
values = YamlText(before or "")
|
|
582
|
+
env = values.get(("env",))
|
|
583
|
+
if not isinstance(env, dict) or not set(EXCHANGE_VARIABLES) & set(env):
|
|
584
|
+
continue
|
|
585
|
+
for variable in EXCHANGE_VARIABLES:
|
|
586
|
+
if variable in values.get(("env",)):
|
|
587
|
+
values.delete(("env", variable))
|
|
588
|
+
text = values.text
|
|
589
|
+
lines = text.splitlines(keepends=True)
|
|
590
|
+
kept = [line for line in lines if line.strip() != f"# {EXCHANGE_VALUES_COMMENT}"]
|
|
591
|
+
if len(kept) != len(lines) and YamlText("".join(kept)).data == values.data:
|
|
592
|
+
text = "".join(kept)
|
|
593
|
+
except EditError as exc:
|
|
594
|
+
plan.left_for_you.append(
|
|
595
|
+
f"{rel}: remove {', '.join(EXCHANGE_VARIABLES)} from env: (not edited: {exc})"
|
|
596
|
+
)
|
|
597
|
+
continue
|
|
598
|
+
plan.set_text(rel, stored, text)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def values_env_set(
|
|
602
|
+
plan: Plan, config: ProjectConfig, variable: str, template: str | Mapping[str, str]
|
|
603
|
+
) -> list[str]:
|
|
604
|
+
"""Set ``variable`` in each ``values-<env>.yaml``'s ``env`` to ``template`` with ``{env}``
|
|
605
|
+
filled, or to the value a mapping gives each environment (the files of the others are
|
|
606
|
+
left alone); the environments it was set for."""
|
|
607
|
+
done = []
|
|
608
|
+
for path in chart_values_files(plan.root, config)[1:]:
|
|
609
|
+
environment = path.stem.removeprefix("values-")
|
|
610
|
+
if isinstance(template, Mapping):
|
|
611
|
+
if environment not in template:
|
|
612
|
+
continue
|
|
613
|
+
value = template[environment]
|
|
614
|
+
else:
|
|
615
|
+
value = template.replace("{env}", environment)
|
|
616
|
+
rel = path.relative_to(plan.root).as_posix()
|
|
617
|
+
before = plan.current(rel)
|
|
618
|
+
try:
|
|
619
|
+
values = YamlText(before or "")
|
|
620
|
+
env = values.get(("env",))
|
|
621
|
+
if env is None:
|
|
622
|
+
values.set(("env",), {variable: value})
|
|
623
|
+
elif not isinstance(env, dict):
|
|
624
|
+
raise EditError(f"{rel} has an env: that is not a mapping")
|
|
625
|
+
elif env.get(variable) != value:
|
|
626
|
+
values.set(("env", variable), value)
|
|
627
|
+
except EditError as exc:
|
|
628
|
+
plan.left_for_you.append(f"{rel}: set env.{variable}: {value} (not edited: {exc})")
|
|
629
|
+
continue
|
|
630
|
+
plan.set_text(rel, read_text(path), values.text)
|
|
631
|
+
done.append(environment)
|
|
632
|
+
return done
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def values_remove(plan: Plan, config: ProjectConfig, variable: str, remaining: set[str]) -> None:
|
|
636
|
+
"""Drop ``variable`` from the chart's env maps; ``remaining``: the other APIs' base URLs.
|
|
637
|
+
|
|
638
|
+
When no API base URL is left in ``values.yaml``, its heading comment goes too.
|
|
639
|
+
"""
|
|
640
|
+
for path in chart_values_files(plan.root, config):
|
|
641
|
+
rel = path.relative_to(plan.root).as_posix()
|
|
642
|
+
before = plan.current(rel)
|
|
643
|
+
try:
|
|
644
|
+
values = YamlText(before or "")
|
|
645
|
+
env = values.get(("env",))
|
|
646
|
+
if not isinstance(env, dict) or variable not in env:
|
|
647
|
+
continue
|
|
648
|
+
values.delete(("env", variable))
|
|
649
|
+
text = values.text
|
|
650
|
+
if not remaining & set(env):
|
|
651
|
+
lines = text.splitlines(keepends=True)
|
|
652
|
+
kept = [line for line in lines if line.strip() != f"# {VALUES_COMMENT}"]
|
|
653
|
+
if len(kept) == len(lines) - 1 and YamlText("".join(kept)).data == values.data:
|
|
654
|
+
text = "".join(kept)
|
|
655
|
+
except EditError as exc:
|
|
656
|
+
plan.left_for_you.append(f"{rel}: remove {variable} from env: (not edited: {exc})")
|
|
657
|
+
continue
|
|
658
|
+
plan.set_text(rel, read_text(path), text)
|