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,462 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
# Modifications Copyright 2026 graph-agents-cli contributors
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Project manifest reader (``graph-agents-cli-manifest.yaml``) and prerequisite guards.
|
|
17
|
+
|
|
18
|
+
The manifest fields are defined by the ProjectInfo model below. There is no legacy
|
|
19
|
+
``pyproject.toml`` fallback: the manifest is the only project marker.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
from collections.abc import Mapping
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
from graph_agents_cli._api_policy import ApiPolicyConfigError, manifest_policy_file_problem
|
|
35
|
+
from graph_agents_cli._defaults import (
|
|
36
|
+
DEFAULT_AGENT_GUIDANCE_FILENAME,
|
|
37
|
+
DEFAULT_CD,
|
|
38
|
+
DEFAULT_MODEL_PROVIDER,
|
|
39
|
+
DEFAULT_MODELS,
|
|
40
|
+
DEFAULT_RUNTIME,
|
|
41
|
+
ENVIRONMENTS,
|
|
42
|
+
PROVIDER_KEY_VARS,
|
|
43
|
+
auth_policy_implemented_default,
|
|
44
|
+
default_secret_keys,
|
|
45
|
+
normalize_auth_policy,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
|
|
49
|
+
API_POLICY_FILENAME = "api-policy.yaml"
|
|
50
|
+
|
|
51
|
+
# Exit code of a configuration error (0 ok, 1 refused or failed gate, 2 tool
|
|
52
|
+
# failure, 3 configuration error).
|
|
53
|
+
EXIT_CONFIG_ERROR = 3
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ManifestError(click.ClickException):
|
|
57
|
+
"""The project manifest cannot be read or has the wrong shape (exit 3)."""
|
|
58
|
+
|
|
59
|
+
exit_code = EXIT_CONFIG_ERROR
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NotInProjectError(click.ClickException):
|
|
63
|
+
"""The command needs a project and none encloses the working directory (exit 3)."""
|
|
64
|
+
|
|
65
|
+
exit_code = EXIT_CONFIG_ERROR
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class SecretsConfig:
|
|
70
|
+
"""The ``secrets:`` block: allow-listed Secret keys and their owner."""
|
|
71
|
+
|
|
72
|
+
keys: list[str] = field(default_factory=list)
|
|
73
|
+
owner: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ApiPolicyConfig:
|
|
78
|
+
"""The ``api_policy:`` block. ``policy_file`` is None when no policy is declared."""
|
|
79
|
+
|
|
80
|
+
policy_file: str | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ProjectConfig:
|
|
85
|
+
"""Configuration read from ``graph-agents-cli-manifest.yaml``.
|
|
86
|
+
|
|
87
|
+
Every manifest key is an attribute. ``create_params`` is kept as the raw
|
|
88
|
+
dict (for ``generation_metadata``) and also exposed through typed
|
|
89
|
+
properties with the defaults of ``graph_agents_cli._defaults``.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
project_name: str = ""
|
|
93
|
+
cli_version: str = ""
|
|
94
|
+
# The ``cli_build`` block as written (``scaffold.utils.build_record`` parses
|
|
95
|
+
# and validates it): the build that rendered the project. None when absent.
|
|
96
|
+
cli_build: Any = None
|
|
97
|
+
agent_directory: str = "app"
|
|
98
|
+
base_template: str = "langgraph"
|
|
99
|
+
generated_at: str = ""
|
|
100
|
+
language: str = "python"
|
|
101
|
+
create_params: dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
environments: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
103
|
+
secrets: SecretsConfig = field(default_factory=SecretsConfig)
|
|
104
|
+
api_policy: ApiPolicyConfig = field(default_factory=ApiPolicyConfig)
|
|
105
|
+
process: str | None = None
|
|
106
|
+
# True when the manifest still has the retired ``product_api:`` block;
|
|
107
|
+
# create/enhance/upgrade/lint stop with a migration message (exit 3).
|
|
108
|
+
has_legacy_product_api: bool = False
|
|
109
|
+
|
|
110
|
+
# -- manifest aliases -------------------------------------------------
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def name(self) -> str:
|
|
114
|
+
"""The manifest's ``name`` key (alias of ``project_name``)."""
|
|
115
|
+
return self.project_name
|
|
116
|
+
|
|
117
|
+
# -- create_params, typed ----------------------------------------------
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def deployment_target(self) -> str:
|
|
121
|
+
return str(self.create_params.get("deployment_target") or "none")
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def runtime(self) -> str:
|
|
125
|
+
return str(self.create_params.get("runtime") or DEFAULT_RUNTIME)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def model_provider(self) -> str:
|
|
129
|
+
return str(self.create_params.get("model_provider") or DEFAULT_MODEL_PROVIDER)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def model(self) -> str:
|
|
133
|
+
recorded = self.create_params.get("model")
|
|
134
|
+
if recorded:
|
|
135
|
+
return str(recorded)
|
|
136
|
+
return DEFAULT_MODELS.get(self.model_provider, "")
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def checkpointer(self) -> str:
|
|
140
|
+
recorded = self.create_params.get("checkpointer")
|
|
141
|
+
if recorded:
|
|
142
|
+
return str(recorded)
|
|
143
|
+
# A deployed project defaults to postgres; a local-only project to memory.
|
|
144
|
+
return "postgres" if self.deployment_target == "kubernetes" else "memory"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def registry(self) -> str:
|
|
148
|
+
return str(self.create_params.get("registry") or "")
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def cd(self) -> str:
|
|
152
|
+
return str(self.create_params.get("cd") or DEFAULT_CD)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def auth_policy(self) -> str:
|
|
156
|
+
"""The recorded policy; the retired ``product-session`` reads as ``custom`` (with a warning)."""
|
|
157
|
+
return normalize_auth_policy(self.create_params.get("auth_policy"))
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def auth_policy_implemented(self) -> bool:
|
|
161
|
+
recorded = self.create_params.get("auth_policy_implemented")
|
|
162
|
+
if recorded is None:
|
|
163
|
+
return auth_policy_implemented_default(self.auth_policy)
|
|
164
|
+
return bool(recorded)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def agent_guidance_filename(self) -> str:
|
|
168
|
+
return str(
|
|
169
|
+
self.create_params.get("agent_guidance_filename") or DEFAULT_AGENT_GUIDANCE_FILENAME
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# -- convenience --------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def secret_keys(self) -> list[str]:
|
|
176
|
+
return list(self.secrets.keys)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def api_policy_file(self) -> str | None:
|
|
180
|
+
return self.api_policy.policy_file
|
|
181
|
+
|
|
182
|
+
def provider_key_var(self) -> str:
|
|
183
|
+
"""The Secret key that carries the model provider's API key."""
|
|
184
|
+
return PROVIDER_KEY_VARS.get(self.model_provider, "MODEL_API_KEY")
|
|
185
|
+
|
|
186
|
+
def environment(self, env: str) -> tuple[str, str]:
|
|
187
|
+
"""Return ``(context, namespace)`` for ``env``.
|
|
188
|
+
|
|
189
|
+
The manifest's ``environments:`` block wins. An environment the block
|
|
190
|
+
does not name but the CLI knows (dev, staging, prod) falls back to an
|
|
191
|
+
empty context (the current kube context) and the ``<name>-<env>``
|
|
192
|
+
namespace convention. Anything else is a usage error.
|
|
193
|
+
"""
|
|
194
|
+
recorded = self.environments.get(env)
|
|
195
|
+
if recorded is not None:
|
|
196
|
+
context = str(recorded.get("context") or "")
|
|
197
|
+
namespace = str(recorded.get("namespace") or f"{self.project_name}-{env}")
|
|
198
|
+
return context, namespace
|
|
199
|
+
if env in ENVIRONMENTS:
|
|
200
|
+
return "", f"{self.project_name}-{env}"
|
|
201
|
+
known = ", ".join(sorted(set(self.environments) | set(ENVIRONMENTS)))
|
|
202
|
+
raise click.UsageError(
|
|
203
|
+
f"Unknown environment '{env}'. Known environments: {known}.\n"
|
|
204
|
+
f" Add it under environments: in {MANIFEST_FILENAME}."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def from_dict(
|
|
209
|
+
cls,
|
|
210
|
+
data: Mapping[str, Any] | Any,
|
|
211
|
+
filename: str = MANIFEST_FILENAME,
|
|
212
|
+
) -> ProjectConfig:
|
|
213
|
+
"""Create a ProjectConfig from a raw manifest mapping."""
|
|
214
|
+
if not isinstance(data, Mapping):
|
|
215
|
+
raise ManifestError(f"malformed {filename}")
|
|
216
|
+
|
|
217
|
+
cfg = cls()
|
|
218
|
+
cfg.project_name = str(data.get("name") or cfg.project_name)
|
|
219
|
+
cfg.cli_version = str(data.get("cli_version") or cfg.cli_version)
|
|
220
|
+
cfg.cli_build = data.get("cli_build")
|
|
221
|
+
cfg.agent_directory = str(data.get("agent_directory") or cfg.agent_directory)
|
|
222
|
+
cfg.base_template = str(data.get("base_template") or cfg.base_template)
|
|
223
|
+
cfg.generated_at = str(data.get("generated_at") or cfg.generated_at)
|
|
224
|
+
cfg.language = str(data.get("language") or cfg.language)
|
|
225
|
+
|
|
226
|
+
create_params = data.get("create_params")
|
|
227
|
+
if create_params is None:
|
|
228
|
+
create_params = {}
|
|
229
|
+
elif not isinstance(create_params, Mapping):
|
|
230
|
+
raise ManifestError(f"malformed create_params in {filename}")
|
|
231
|
+
cfg.create_params = dict(create_params)
|
|
232
|
+
|
|
233
|
+
environments = data.get("environments") or {}
|
|
234
|
+
if not isinstance(environments, Mapping):
|
|
235
|
+
raise ManifestError(f"malformed environments in {filename}")
|
|
236
|
+
parsed_envs: dict[str, dict[str, str]] = {}
|
|
237
|
+
for env_name, env_data in environments.items():
|
|
238
|
+
if env_data is None:
|
|
239
|
+
env_data = {}
|
|
240
|
+
if not isinstance(env_data, Mapping):
|
|
241
|
+
raise ManifestError(f"malformed environments.{env_name} in {filename}")
|
|
242
|
+
parsed_envs[str(env_name)] = {
|
|
243
|
+
"context": str(env_data.get("context") or ""),
|
|
244
|
+
"namespace": str(env_data.get("namespace") or f"{cfg.project_name}-{env_name}"),
|
|
245
|
+
}
|
|
246
|
+
cfg.environments = parsed_envs
|
|
247
|
+
|
|
248
|
+
secrets = data.get("secrets") or {}
|
|
249
|
+
if not isinstance(secrets, Mapping):
|
|
250
|
+
raise ManifestError(f"malformed secrets in {filename}")
|
|
251
|
+
raw_keys = secrets.get("keys")
|
|
252
|
+
if isinstance(raw_keys, str):
|
|
253
|
+
raw_keys = [raw_keys]
|
|
254
|
+
keys = [str(k) for k in raw_keys] if raw_keys else []
|
|
255
|
+
if not keys:
|
|
256
|
+
# No recorded allow-list: the default follows the provider and runtime.
|
|
257
|
+
keys = default_secret_keys(cfg.model_provider, cfg.runtime, auth_policy=cfg.auth_policy)
|
|
258
|
+
cfg.secrets = SecretsConfig(keys=keys, owner=str(secrets.get("owner") or ""))
|
|
259
|
+
|
|
260
|
+
api_policy = data.get("api_policy") or {}
|
|
261
|
+
if not isinstance(api_policy, Mapping):
|
|
262
|
+
raise ManifestError(f"malformed api_policy in {filename}")
|
|
263
|
+
policy_file = api_policy.get("policy_file")
|
|
264
|
+
problem = manifest_policy_file_problem(policy_file, filename)
|
|
265
|
+
if problem:
|
|
266
|
+
raise ApiPolicyConfigError(problem)
|
|
267
|
+
cfg.api_policy = ApiPolicyConfig(policy_file=API_POLICY_FILENAME if policy_file else None)
|
|
268
|
+
cfg.has_legacy_product_api = "product_api" in data
|
|
269
|
+
|
|
270
|
+
process = data.get("process")
|
|
271
|
+
cfg.process = str(process) if process else None
|
|
272
|
+
|
|
273
|
+
return cfg
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _read_project_config_from_manifest(manifest_path: Path) -> ProjectConfig:
|
|
277
|
+
"""Read project configuration from a manifest file (unreadable or invalid YAML: exit 3)."""
|
|
278
|
+
try:
|
|
279
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
280
|
+
data = yaml.safe_load(f)
|
|
281
|
+
except yaml.YAMLError as exc:
|
|
282
|
+
where = ""
|
|
283
|
+
mark = getattr(exc, "problem_mark", None)
|
|
284
|
+
if mark is not None:
|
|
285
|
+
where = f" (line {mark.line + 1}, column {mark.column + 1})"
|
|
286
|
+
problem = getattr(exc, "problem", None) or str(exc).splitlines()[0]
|
|
287
|
+
raise ManifestError(
|
|
288
|
+
f"{manifest_path} is not valid YAML{where}: {problem}.\n"
|
|
289
|
+
" Fix the file, or restore it from version control."
|
|
290
|
+
) from None
|
|
291
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
292
|
+
raise ManifestError(f"Cannot read {manifest_path}: {exc}") from None
|
|
293
|
+
if data is None:
|
|
294
|
+
data = {}
|
|
295
|
+
return ProjectConfig.from_dict(data, filename=manifest_path.name)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def read_project_config(project_dir: str | None = None) -> ProjectConfig:
|
|
299
|
+
"""Read project metadata from ``graph-agents-cli-manifest.yaml``.
|
|
300
|
+
|
|
301
|
+
Returns a default ``ProjectConfig`` when the directory holds no manifest.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
project_dir: Directory containing the manifest. Defaults to the
|
|
305
|
+
current working directory.
|
|
306
|
+
"""
|
|
307
|
+
root = Path(project_dir) if project_dir else Path.cwd()
|
|
308
|
+
manifest_path = root / MANIFEST_FILENAME
|
|
309
|
+
if manifest_path.exists():
|
|
310
|
+
return _read_project_config_from_manifest(manifest_path)
|
|
311
|
+
return ProjectConfig()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _parse_version(v: str) -> tuple[int, ...]:
|
|
315
|
+
"""Parse a dotted numeric version into a comparable tuple.
|
|
316
|
+
|
|
317
|
+
Raises ``ValueError`` on non-numeric components (e.g. a ``0.0.0-dev``
|
|
318
|
+
source build), which callers treat as "version unknown, don't compare".
|
|
319
|
+
"""
|
|
320
|
+
return tuple(int(x) for x in v.split("."))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def scaffold_older_than(cfg: ProjectConfig, version: str) -> bool:
|
|
324
|
+
"""True when the version that scaffolded ``cfg`` is older than ``version``.
|
|
325
|
+
|
|
326
|
+
Best-effort: returns False when the scaffold version is missing or
|
|
327
|
+
non-numeric, so callers fall back to generic guidance instead of guessing.
|
|
328
|
+
"""
|
|
329
|
+
if not cfg.cli_version:
|
|
330
|
+
return False
|
|
331
|
+
try:
|
|
332
|
+
return _parse_version(cfg.cli_version) < _parse_version(version)
|
|
333
|
+
except ValueError:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def check_cli_version(cfg: ProjectConfig) -> None:
|
|
338
|
+
"""Warn if the running CLI version differs from the one that scaffolded the project.
|
|
339
|
+
|
|
340
|
+
Emits a warning with upgrade guidance on a mismatch; never blocks execution.
|
|
341
|
+
"""
|
|
342
|
+
cli_version = cfg.cli_version
|
|
343
|
+
if not cli_version:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
from graph_agents_cli import __version__
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
project_ver = _parse_version(cli_version)
|
|
350
|
+
cli_ver = _parse_version(__version__)
|
|
351
|
+
except ValueError:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
if cli_ver < project_ver:
|
|
355
|
+
from graph_agents_cli.scaffold.utils.version import install_command
|
|
356
|
+
|
|
357
|
+
click.echo(
|
|
358
|
+
f"\n⚠️ Version mismatch: project was scaffolded with graph-agents-cli {cli_version},"
|
|
359
|
+
f" running {__version__}.\n"
|
|
360
|
+
f" Upgrade the CLI: {install_command(version=cli_version)}\n",
|
|
361
|
+
err=True,
|
|
362
|
+
)
|
|
363
|
+
elif cli_ver > project_ver:
|
|
364
|
+
click.echo(
|
|
365
|
+
f"\n⚠️ Version mismatch: project was scaffolded with graph-agents-cli {cli_version},"
|
|
366
|
+
f" running {__version__}.\n"
|
|
367
|
+
" Upgrade the project: graph-agents-cli scaffold upgrade\n",
|
|
368
|
+
err=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def find_project_root(dir: Path | None = None) -> Path | None:
|
|
373
|
+
"""Find the project root by walking up looking for the manifest."""
|
|
374
|
+
if dir is None:
|
|
375
|
+
dir = Path.cwd()
|
|
376
|
+
for parent in [dir, *dir.parents]:
|
|
377
|
+
if (parent / MANIFEST_FILENAME).exists():
|
|
378
|
+
return parent
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def is_project_moved() -> bool:
|
|
383
|
+
"""Check if the project has been moved by comparing the current path with .venv/bin/activate."""
|
|
384
|
+
root = find_project_root(Path.cwd())
|
|
385
|
+
if not root:
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
venv_dir = root / ".venv"
|
|
389
|
+
# Support both Unix-style (bin) and Windows-style (Scripts) virtualenvs
|
|
390
|
+
activate_script = venv_dir / "bin" / "activate"
|
|
391
|
+
if not activate_script.exists():
|
|
392
|
+
activate_script = venv_dir / "Scripts" / "activate"
|
|
393
|
+
|
|
394
|
+
if not activate_script.exists():
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
with open(activate_script, encoding="utf-8") as f:
|
|
399
|
+
for line in f:
|
|
400
|
+
if line.startswith("VIRTUAL_ENV="):
|
|
401
|
+
stored_path_str = line.split("=", 1)[1].strip().strip("'\"")
|
|
402
|
+
stored_path = Path(stored_path_str).resolve()
|
|
403
|
+
current_path = venv_dir.resolve()
|
|
404
|
+
return stored_path != current_path
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logging.warning(f"Error checking if project moved: {e}")
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Prerequisite guards, reusable checks for CLI commands
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def chdir_project_root(dir: Path | None = None) -> None:
|
|
416
|
+
"""Locate the project root relative to ``dir`` and chdir to it; raise if none."""
|
|
417
|
+
if dir is None:
|
|
418
|
+
dir = Path.cwd()
|
|
419
|
+
root = find_project_root(dir)
|
|
420
|
+
if not root:
|
|
421
|
+
raise NotInProjectError(
|
|
422
|
+
f"No {MANIFEST_FILENAME} found in the current directory or its parents.\n"
|
|
423
|
+
" Run this command from your project root, or create a project first:\n"
|
|
424
|
+
" graph-agents-cli create my-agent"
|
|
425
|
+
)
|
|
426
|
+
# Only announce the root when we actually move (i.e. run from a subdir).
|
|
427
|
+
if root.resolve() != Path.cwd().resolve():
|
|
428
|
+
click.echo(f"Using project root directory: {root}", err=True)
|
|
429
|
+
os.chdir(root)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def require_agent_directory(cfg: ProjectConfig) -> None:
|
|
433
|
+
"""Raise if the configured agent_directory doesn't exist. Assumes cwd is the project root."""
|
|
434
|
+
agent_path = Path(cfg.agent_directory)
|
|
435
|
+
if not agent_path.is_dir():
|
|
436
|
+
raise click.ClickException(
|
|
437
|
+
f"Agent directory '{cfg.agent_directory}' not found.\n"
|
|
438
|
+
" Ensure you're in the project root and that the directory exists.\n"
|
|
439
|
+
f" The agent_directory is configured in {MANIFEST_FILENAME}."
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def require_deployment_target(cfg: ProjectConfig) -> None:
|
|
444
|
+
"""Raise a UsageError when the project has no deployment target (``none``)."""
|
|
445
|
+
if cfg.deployment_target in ("none", ""):
|
|
446
|
+
raise click.UsageError(
|
|
447
|
+
"No deployment target configured (deployment_target is 'none').\n"
|
|
448
|
+
f" Set create_params.deployment_target in {MANIFEST_FILENAME},\n"
|
|
449
|
+
" or add deployment support to your project:\n"
|
|
450
|
+
" graph-agents-cli scaffold enhance --deployment-target kubernetes"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def find_project_config(project_dir: Path | None = None) -> ProjectConfig | None:
|
|
455
|
+
"""Read the manifest after resolving the project root from ``project_dir``.
|
|
456
|
+
|
|
457
|
+
Returns None when no project root is found.
|
|
458
|
+
"""
|
|
459
|
+
project_root_dir = find_project_root(project_dir)
|
|
460
|
+
if project_root_dir is None:
|
|
461
|
+
return None
|
|
462
|
+
return read_project_config(str(project_root_dir))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
# Modifications Copyright 2026 graph-agents-cli contributors
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Request headers and URL classification for talking to a remote agent.
|
|
17
|
+
|
|
18
|
+
Transport-neutral so every caller (``run``, ``eval generate``) agrees on which
|
|
19
|
+
credentials a ``--url`` gets and where the chat or A2A surface lives.
|
|
20
|
+
Credentials follow the project's auth policy:
|
|
21
|
+
|
|
22
|
+
* ``--header 'Authorization: Bearer ...'`` or ``GRAPH_AGENTS_CLI_API_KEY``
|
|
23
|
+
for ``shared-bearer`` (and a JWT in the same header for ``jwt``);
|
|
24
|
+
* ``--header 'Name: value'`` or ``--cookie name=value`` for a ``custom``
|
|
25
|
+
policy. The hidden ``--session-token`` option of ``run`` and ``eval`` is a
|
|
26
|
+
deprecated alias of ``--header 'X-Session-Token: ...'``: it still works, prints
|
|
27
|
+
a one-line deprecation warning, and is folded into the headers on entry.
|
|
28
|
+
|
|
29
|
+
No cloud SDK is consulted; the CLI stores no credentials.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
from collections.abc import Mapping, Sequence
|
|
36
|
+
from typing import NamedTuple
|
|
37
|
+
|
|
38
|
+
import click
|
|
39
|
+
import httpx
|
|
40
|
+
|
|
41
|
+
from graph_agents_cli import _http
|
|
42
|
+
|
|
43
|
+
API_KEY_ENV = "GRAPH_AGENTS_CLI_API_KEY"
|
|
44
|
+
SESSION_TOKEN_HEADER = "X-Session-Token"
|
|
45
|
+
AGENT_CARD_PATH = "/.well-known/agent-card.json"
|
|
46
|
+
|
|
47
|
+
RUN_MODES: tuple[str, ...] = ("chat", "a2a")
|
|
48
|
+
DEFAULT_RUN_MODE = "chat"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SESSION_TOKEN_DEPRECATION = (
|
|
52
|
+
"Warning: --session-token is deprecated and will be removed; "
|
|
53
|
+
"pass --header 'X-Session-Token: <token>' instead."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def deprecated_session_token(
|
|
58
|
+
_ctx: click.Context, _param: click.Parameter, value: str | None
|
|
59
|
+
) -> str | None:
|
|
60
|
+
"""Click callback of the hidden ``--session-token`` alias: warn once when it is used."""
|
|
61
|
+
if value:
|
|
62
|
+
click.echo(SESSION_TOKEN_DEPRECATION, err=True)
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def fold_session_token(header: Sequence[str], session_token: str | None) -> tuple[str, ...]:
|
|
67
|
+
"""``header`` plus the deprecated ``--session-token`` value as its ``X-Session-Token`` header.
|
|
68
|
+
|
|
69
|
+
Folding it into the ``--header`` values once, on entry, means everything
|
|
70
|
+
downstream (resume hints, extension overrides) only ever sees ``--header``.
|
|
71
|
+
"""
|
|
72
|
+
if not session_token:
|
|
73
|
+
return tuple(header)
|
|
74
|
+
return (*header, f"{SESSION_TOKEN_HEADER}: {session_token}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_header(value: str) -> tuple[str, str]:
|
|
78
|
+
"""Parse one ``Key: Value`` header string."""
|
|
79
|
+
if ":" not in value:
|
|
80
|
+
raise click.BadParameter(f"Invalid header format (expected 'Key: Value'): {value}")
|
|
81
|
+
key, _, val = value.partition(":")
|
|
82
|
+
key = key.strip()
|
|
83
|
+
if not key:
|
|
84
|
+
raise click.BadParameter(f"Invalid header format (empty name): {value}")
|
|
85
|
+
return key, val.strip()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_cookie(value: str) -> tuple[str, str]:
|
|
89
|
+
"""Parse one ``name=value`` cookie string."""
|
|
90
|
+
if "=" not in value:
|
|
91
|
+
raise click.BadParameter(f"Invalid cookie format (expected 'name=value'): {value}")
|
|
92
|
+
name, _, val = value.partition("=")
|
|
93
|
+
name = name.strip()
|
|
94
|
+
if not name:
|
|
95
|
+
raise click.BadParameter(f"Invalid cookie format (empty name): {value}")
|
|
96
|
+
return name, val.strip()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _has_header(headers: Mapping[str, str], name: str) -> bool:
|
|
100
|
+
lname = name.lower()
|
|
101
|
+
return any(k.lower() == lname for k in headers)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_headers(
|
|
105
|
+
header: Sequence[str] = (),
|
|
106
|
+
cookie: Sequence[str] = (),
|
|
107
|
+
session_token: str | None = None,
|
|
108
|
+
env: Mapping[str, str] | None = None,
|
|
109
|
+
) -> dict[str, str]:
|
|
110
|
+
"""Build the request headers for a chat or A2A call.
|
|
111
|
+
|
|
112
|
+
Precedence: explicit ``--header`` values are applied verbatim; ``--cookie``
|
|
113
|
+
pairs are folded into one ``Cookie`` header (appended to an explicit
|
|
114
|
+
``Cookie`` header if one was given); ``--session-token`` becomes
|
|
115
|
+
``X-Session-Token``; and when no ``Authorization`` header was supplied,
|
|
116
|
+
``GRAPH_AGENTS_CLI_API_KEY`` from ``env`` (default ``os.environ``) becomes
|
|
117
|
+
``Authorization: Bearer <key>``.
|
|
118
|
+
"""
|
|
119
|
+
environ = os.environ if env is None else env
|
|
120
|
+
headers: dict[str, str] = {}
|
|
121
|
+
for raw in header:
|
|
122
|
+
key, val = parse_header(raw)
|
|
123
|
+
headers[key] = val
|
|
124
|
+
|
|
125
|
+
pairs = [parse_cookie(c) for c in cookie]
|
|
126
|
+
if pairs:
|
|
127
|
+
rendered = "; ".join(f"{k}={v}" for k, v in pairs)
|
|
128
|
+
existing_key = next((k for k in headers if k.lower() == "cookie"), None)
|
|
129
|
+
if existing_key is not None and headers[existing_key]:
|
|
130
|
+
headers[existing_key] = f"{headers[existing_key]}; {rendered}"
|
|
131
|
+
else:
|
|
132
|
+
headers[existing_key or "Cookie"] = rendered
|
|
133
|
+
|
|
134
|
+
if session_token:
|
|
135
|
+
headers[SESSION_TOKEN_HEADER] = session_token
|
|
136
|
+
|
|
137
|
+
if not _has_header(headers, "Authorization"):
|
|
138
|
+
api_key = environ.get(API_KEY_ENV, "")
|
|
139
|
+
if api_key:
|
|
140
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
141
|
+
|
|
142
|
+
return headers
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RemoteTarget(NamedTuple):
|
|
146
|
+
"""Where a query goes: the chat base URL or the resolved A2A base URL."""
|
|
147
|
+
|
|
148
|
+
mode: str
|
|
149
|
+
base_url: str
|
|
150
|
+
# For ``a2a``: the base under which the agent card was found. ``None``
|
|
151
|
+
# for ``chat``.
|
|
152
|
+
a2a_base: str | None = None
|
|
153
|
+
card: dict | None = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def a2a_candidates(base_url: str, agent_directory: str | None) -> list[str]:
|
|
157
|
+
"""A2A base URLs to probe for a card, in order.
|
|
158
|
+
|
|
159
|
+
The scaffolded app serves the card at ``/a2a/<agent_directory>``; the bare URL is tried second so a ``--url`` that already points
|
|
160
|
+
at the A2A base, or a spec-canonical root card, still works.
|
|
161
|
+
"""
|
|
162
|
+
base = base_url.rstrip("/")
|
|
163
|
+
candidates: list[str] = []
|
|
164
|
+
if agent_directory:
|
|
165
|
+
candidates.append(f"{base}/a2a/{agent_directory}")
|
|
166
|
+
candidates.append(base)
|
|
167
|
+
return candidates
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def classify_url(
|
|
171
|
+
url: str,
|
|
172
|
+
*,
|
|
173
|
+
mode: str = DEFAULT_RUN_MODE,
|
|
174
|
+
agent_directory: str | None = None,
|
|
175
|
+
headers: Mapping[str, str] | None = None,
|
|
176
|
+
timeout: float = 30.0,
|
|
177
|
+
) -> RemoteTarget:
|
|
178
|
+
"""Resolve a ``--url`` into a :class:`RemoteTarget` for ``mode``.
|
|
179
|
+
|
|
180
|
+
``chat`` needs no probing: the chat API lives at ``<url>/chat``. ``a2a``
|
|
181
|
+
probes ``<url>/a2a/<agent_directory>/.well-known/agent-card.json`` and then
|
|
182
|
+
``<url>/.well-known/agent-card.json``; the first 200 wins.
|
|
183
|
+
|
|
184
|
+
Raises :class:`click.ClickException` when no card is found (listing each
|
|
185
|
+
probe) or :class:`click.BadParameter` for an unknown mode.
|
|
186
|
+
"""
|
|
187
|
+
base = url.rstrip("/")
|
|
188
|
+
if mode == "chat":
|
|
189
|
+
return RemoteTarget(mode="chat", base_url=base)
|
|
190
|
+
if mode != "a2a":
|
|
191
|
+
raise click.BadParameter(f"Unknown mode {mode!r}; choose from {', '.join(RUN_MODES)}")
|
|
192
|
+
|
|
193
|
+
failures: list[tuple[str, str]] = []
|
|
194
|
+
for candidate in a2a_candidates(base, agent_directory):
|
|
195
|
+
card_url = f"{candidate}{AGENT_CARD_PATH}"
|
|
196
|
+
try:
|
|
197
|
+
resp = _http.get(card_url, headers=dict(headers or {}), timeout=timeout)
|
|
198
|
+
except httpx.TransportError as exc:
|
|
199
|
+
failures.append((card_url, f"unreachable: {exc}"))
|
|
200
|
+
continue
|
|
201
|
+
if resp.status_code == 200:
|
|
202
|
+
try:
|
|
203
|
+
card = resp.json()
|
|
204
|
+
except ValueError:
|
|
205
|
+
card = None
|
|
206
|
+
return RemoteTarget(
|
|
207
|
+
mode="a2a",
|
|
208
|
+
base_url=base,
|
|
209
|
+
a2a_base=candidate,
|
|
210
|
+
card=card if isinstance(card, dict) else None,
|
|
211
|
+
)
|
|
212
|
+
failures.append((card_url, f"HTTP {resp.status_code}: {resp.text[:200]}"))
|
|
213
|
+
|
|
214
|
+
detail = "\n".join(f" {card_url}\n {why}" for card_url, why in failures)
|
|
215
|
+
hint = ""
|
|
216
|
+
if base.endswith("/a2a"):
|
|
217
|
+
hint += "\n Pass the base service URL without the '/a2a' suffix."
|
|
218
|
+
if any("HTTP 404" in why or "HTTP 405" in why for _, why in failures):
|
|
219
|
+
hint += "\n If this agent only exposes the chat API, try --mode chat instead."
|
|
220
|
+
raise click.ClickException(f"Failed to fetch an A2A agent card:\n{detail}{hint}")
|