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,273 @@
|
|
|
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
|
+
"""Load a locally built image into a single-node dev cluster (kind, k3d, k3s, minikube, ...).
|
|
15
|
+
|
|
16
|
+
A context name is only a label: a renamed kind context would otherwise make
|
|
17
|
+
``deploy`` push a dev build to the real registry, and a production context that
|
|
18
|
+
happens to be called ``kind-...`` would be treated as a laptop cluster. So the
|
|
19
|
+
cluster is identified from the cluster itself: its nodes' ``providerID``, name
|
|
20
|
+
and labels (kind ``kind://<provider>/<cluster>/<node>``, k3d ``k3d-<cluster>-server-N``,
|
|
21
|
+
k3s ``k3s://``, minikube's ``minikube.k8s.io/name`` label, Docker Desktop and
|
|
22
|
+
OrbStack node names). A kind cluster is also confirmed with ``kind get
|
|
23
|
+
clusters``, since ``kind load`` only reaches clusters on this machine. Only
|
|
24
|
+
when the nodes cannot be read (no permission to list nodes, cluster down)
|
|
25
|
+
does the context name decide, and a ``kind-*`` name is still confirmed with
|
|
26
|
+
the kind CLI.
|
|
27
|
+
|
|
28
|
+
Only the ``docker`` CLI is supported for builds; Docker Desktop, Rancher
|
|
29
|
+
Desktop and OrbStack share the daemon with the cluster, so nothing needs
|
|
30
|
+
loading there.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import re
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from graph_agents_cli.deploy import _kube
|
|
42
|
+
|
|
43
|
+
KIND = "kind"
|
|
44
|
+
K3D = "k3d"
|
|
45
|
+
K3S = "k3s"
|
|
46
|
+
MINIKUBE = "minikube"
|
|
47
|
+
SHARED_DAEMON = "shared-daemon"
|
|
48
|
+
|
|
49
|
+
_K3D_NODE = re.compile(r"^k3d-(?P<cluster>.+)-(?:server|agent)-\d+$")
|
|
50
|
+
_SHARED_DAEMON_NODES = ("docker-desktop", "orbstack")
|
|
51
|
+
NODE_READ_TIMEOUT = "10s"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class LocalCluster:
|
|
56
|
+
"""A single-node dev cluster the image can be loaded into."""
|
|
57
|
+
|
|
58
|
+
kind: str
|
|
59
|
+
name: str = ""
|
|
60
|
+
evidence: str = ""
|
|
61
|
+
|
|
62
|
+
def describe(self) -> str:
|
|
63
|
+
label = {
|
|
64
|
+
KIND: f"kind cluster {self.name!r}",
|
|
65
|
+
K3D: f"k3d cluster {self.name!r}",
|
|
66
|
+
K3S: "k3s",
|
|
67
|
+
MINIKUBE: f"minikube profile {self.name!r}",
|
|
68
|
+
SHARED_DAEMON: f"{self.name or 'local cluster'} (shares the docker daemon)",
|
|
69
|
+
}.get(self.kind, self.kind)
|
|
70
|
+
return f"{label}{f', {self.evidence}' if self.evidence else ''}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def from_context_name(context: str | None) -> LocalCluster | None:
|
|
74
|
+
"""The dev cluster a context name suggests (unverified), or ``None``."""
|
|
75
|
+
name = (context or "").strip()
|
|
76
|
+
if name.startswith("kind-"):
|
|
77
|
+
return LocalCluster(KIND, name[len("kind-") :], "from the context name")
|
|
78
|
+
if name.startswith("k3d-"):
|
|
79
|
+
return LocalCluster(K3D, name[len("k3d-") :], "from the context name")
|
|
80
|
+
if name.startswith("minikube"):
|
|
81
|
+
return LocalCluster(MINIKUBE, name, "from the context name")
|
|
82
|
+
if name.startswith("k3s"):
|
|
83
|
+
return LocalCluster(K3S, "", "from the context name")
|
|
84
|
+
if name in ("docker-desktop", "rancher-desktop", "orbstack"):
|
|
85
|
+
return LocalCluster(SHARED_DAEMON, name, "from the context name")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def from_nodes(nodes: list[dict[str, Any]]) -> LocalCluster | None:
|
|
90
|
+
"""The dev cluster the node objects identify, or ``None`` for any other cluster."""
|
|
91
|
+
for node in nodes:
|
|
92
|
+
meta = node.get("metadata") if isinstance(node.get("metadata"), dict) else {}
|
|
93
|
+
spec = node.get("spec") if isinstance(node.get("spec"), dict) else {}
|
|
94
|
+
labels = meta.get("labels") if isinstance(meta.get("labels"), dict) else {}
|
|
95
|
+
node_name = str(meta.get("name") or "")
|
|
96
|
+
provider_id = str(spec.get("providerID") or "")
|
|
97
|
+
if provider_id.startswith("kind://"):
|
|
98
|
+
# kind://<provider>/<cluster>/<node>
|
|
99
|
+
parts = provider_id[len("kind://") :].split("/")
|
|
100
|
+
if len(parts) >= 3 and parts[1]:
|
|
101
|
+
return LocalCluster(KIND, parts[1], f"node providerID {provider_id}")
|
|
102
|
+
if labels.get("minikube.k8s.io/name"):
|
|
103
|
+
profile = str(labels["minikube.k8s.io/name"])
|
|
104
|
+
return LocalCluster(MINIKUBE, profile, f"node label minikube.k8s.io/name={profile}")
|
|
105
|
+
if node_name in _SHARED_DAEMON_NODES or "rancher-desktop" in node_name:
|
|
106
|
+
return LocalCluster(SHARED_DAEMON, node_name, f"node {node_name}")
|
|
107
|
+
if provider_id.startswith("k3s://"):
|
|
108
|
+
match = _K3D_NODE.match(node_name)
|
|
109
|
+
if match:
|
|
110
|
+
return LocalCluster(K3D, match.group("cluster"), f"node {node_name}")
|
|
111
|
+
return LocalCluster(K3S, "", f"node providerID {provider_id}")
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_nodes(context: str | None) -> list[dict[str, Any]] | None:
|
|
116
|
+
"""The cluster's Node objects, ``None`` when they cannot be read (never raises)."""
|
|
117
|
+
target = _kube.Target(context=context, namespace="")
|
|
118
|
+
try:
|
|
119
|
+
result = _kube.kubectl(
|
|
120
|
+
["get", "nodes", "-o", "json", f"--request-timeout={NODE_READ_TIMEOUT}"],
|
|
121
|
+
target,
|
|
122
|
+
namespaced=False,
|
|
123
|
+
check=False,
|
|
124
|
+
quiet=True,
|
|
125
|
+
)
|
|
126
|
+
except _kube.ToolFailed:
|
|
127
|
+
return None
|
|
128
|
+
if result.returncode != 0 or not (result.stdout or "").strip():
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
data = json.loads(result.stdout)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
return None
|
|
134
|
+
items = data.get("items") if isinstance(data, dict) else None
|
|
135
|
+
if not isinstance(items, list) or not items:
|
|
136
|
+
return None
|
|
137
|
+
return [item for item in items if isinstance(item, dict)]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _tool_output(cmd: list[str]) -> str | None:
|
|
141
|
+
"""stdout of a local CLI, ``None`` when it is missing or fails (never raises)."""
|
|
142
|
+
try:
|
|
143
|
+
result = _kube.run_cmd(cmd, check=False, quiet=True)
|
|
144
|
+
except _kube.ToolFailed:
|
|
145
|
+
return None
|
|
146
|
+
if result.returncode != 0:
|
|
147
|
+
return None
|
|
148
|
+
return result.stdout or ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def kind_clusters() -> set[str] | None:
|
|
152
|
+
"""Names ``kind get clusters`` reports on this machine, ``None`` when kind is unavailable."""
|
|
153
|
+
out = _tool_output(["kind", "get", "clusters"])
|
|
154
|
+
if out is None:
|
|
155
|
+
return None
|
|
156
|
+
return {line.strip() for line in out.splitlines() if line.strip()}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _json_or_none(text: str | None) -> Any:
|
|
160
|
+
if text is None:
|
|
161
|
+
return None
|
|
162
|
+
try:
|
|
163
|
+
return json.loads(text or "null")
|
|
164
|
+
except json.JSONDecodeError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def k3d_clusters() -> set[str] | None:
|
|
169
|
+
"""Names ``k3d cluster list`` reports on this machine, ``None`` when k3d is unavailable."""
|
|
170
|
+
data = _json_or_none(_tool_output(["k3d", "cluster", "list", "-o", "json"]))
|
|
171
|
+
if not isinstance(data, list):
|
|
172
|
+
return None
|
|
173
|
+
return {str(c.get("name")) for c in data if isinstance(c, dict) and c.get("name")}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def minikube_profiles() -> set[str] | None:
|
|
177
|
+
"""Valid profiles ``minikube profile list`` reports, ``None`` when minikube is unavailable."""
|
|
178
|
+
data = _json_or_none(_tool_output(["minikube", "profile", "list", "-o", "json"]))
|
|
179
|
+
valid = data.get("valid") if isinstance(data, dict) else None
|
|
180
|
+
if not isinstance(valid, list):
|
|
181
|
+
return None
|
|
182
|
+
return {str(p.get("Name")) for p in valid if isinstance(p, dict) and p.get("Name")}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _local_hostnames() -> set[str]:
|
|
186
|
+
import socket
|
|
187
|
+
|
|
188
|
+
name = socket.gethostname().lower()
|
|
189
|
+
return {name, name.split(".")[0]}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_LISTINGS = {
|
|
193
|
+
KIND: ("kind get clusters", kind_clusters),
|
|
194
|
+
K3D: ("k3d cluster list", k3d_clusters),
|
|
195
|
+
MINIKUBE: ("minikube profile list", minikube_profiles),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _verify(
|
|
200
|
+
cluster: LocalCluster, nodes: list[dict[str, Any]] | None
|
|
201
|
+
) -> tuple[LocalCluster | None, str]:
|
|
202
|
+
"""Confirm that ``cluster`` runs on this machine, where its load command can reach it."""
|
|
203
|
+
if cluster.kind in _LISTINGS:
|
|
204
|
+
command, listing = _LISTINGS[cluster.kind]
|
|
205
|
+
names = listing()
|
|
206
|
+
if names is None:
|
|
207
|
+
return None, (
|
|
208
|
+
f"{cluster.describe()}, but `{command}` is not available on this machine to "
|
|
209
|
+
"load images into it"
|
|
210
|
+
)
|
|
211
|
+
if cluster.name not in names:
|
|
212
|
+
return None, (
|
|
213
|
+
f"{cluster.describe()}, but `{command}` on this machine does not list "
|
|
214
|
+
f"{cluster.name!r}"
|
|
215
|
+
)
|
|
216
|
+
evidence = f"{cluster.evidence}; `{command}` lists it"
|
|
217
|
+
return LocalCluster(cluster.kind, cluster.name, evidence), ""
|
|
218
|
+
if cluster.kind == K3S and nodes is not None:
|
|
219
|
+
# k3s also runs production clusters: only this machine's own node can import images.
|
|
220
|
+
node_names = {
|
|
221
|
+
str(n["metadata"].get("name") or "").lower()
|
|
222
|
+
for n in nodes
|
|
223
|
+
if isinstance(n.get("metadata"), dict)
|
|
224
|
+
}
|
|
225
|
+
if not node_names & _local_hostnames():
|
|
226
|
+
return None, f"{cluster.describe()}, but none of its nodes is this machine"
|
|
227
|
+
return cluster, ""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def detect(context: str | None) -> tuple[LocalCluster | None, str]:
|
|
231
|
+
"""Identify a local dev cluster behind ``context``: ``(cluster or None, reason)``.
|
|
232
|
+
|
|
233
|
+
``reason`` explains, in one line, why the cluster is not treated as local.
|
|
234
|
+
"""
|
|
235
|
+
nodes = read_nodes(context)
|
|
236
|
+
if nodes is not None:
|
|
237
|
+
cluster = from_nodes(nodes)
|
|
238
|
+
if cluster is None:
|
|
239
|
+
return None, "its nodes are not a local dev cluster"
|
|
240
|
+
else:
|
|
241
|
+
cluster = from_context_name(context)
|
|
242
|
+
if cluster is None:
|
|
243
|
+
return None, "its nodes cannot be read and the context name is not a dev cluster"
|
|
244
|
+
verified, why = _verify(cluster, nodes)
|
|
245
|
+
if verified is None:
|
|
246
|
+
# Docker Desktop can run its Kubernetes on kind nodes that the kind CLI does
|
|
247
|
+
# not list; the desktop apps share the docker daemon with the cluster, so
|
|
248
|
+
# their own context names still mean "no load needed" (never a push).
|
|
249
|
+
named = from_context_name(context)
|
|
250
|
+
if named is not None and named.kind == SHARED_DAEMON:
|
|
251
|
+
return named, ""
|
|
252
|
+
return verified, why
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def local_load_commands(
|
|
256
|
+
cluster: LocalCluster, image: str, *, tar_path: Path | None = None
|
|
257
|
+
) -> list[list[str]]:
|
|
258
|
+
"""The commands that make ``image`` visible to ``cluster``.
|
|
259
|
+
|
|
260
|
+
An empty list means the cluster already sees the docker daemon's images.
|
|
261
|
+
"""
|
|
262
|
+
if cluster.kind == KIND:
|
|
263
|
+
return [["kind", "load", "docker-image", image, "--name", cluster.name]]
|
|
264
|
+
if cluster.kind == K3D:
|
|
265
|
+
return [["k3d", "image", "import", image, "-c", cluster.name]]
|
|
266
|
+
if cluster.kind == MINIKUBE:
|
|
267
|
+
if cluster.name and cluster.name != "minikube":
|
|
268
|
+
return [["minikube", "image", "load", image, "-p", cluster.name]]
|
|
269
|
+
return [["minikube", "image", "load", image]]
|
|
270
|
+
if cluster.kind == K3S:
|
|
271
|
+
tar = str(tar_path or Path(".graph-agents-cli") / "image.tar")
|
|
272
|
+
return [["docker", "save", "-o", tar, image], ["k3s", "ctr", "images", "import", tar]]
|
|
273
|
+
return []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
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
|
+
# http://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.
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
"""graph-agents-cli build command: ``docker build`` on the project's Dockerfile.
|
|
17
|
+
|
|
18
|
+
One runtime-specific Dockerfile per project, selected at scaffold time;
|
|
19
|
+
``build``, CI, and local-load all run ``docker build``. There is no experiment
|
|
20
|
+
gate. Exit codes: 0 ok, 2 when docker fails, 3 for a configuration error.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import shlex
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
|
|
30
|
+
from graph_agents_cli import _tools
|
|
31
|
+
from graph_agents_cli._project import chdir_project_root, read_project_config
|
|
32
|
+
from graph_agents_cli._runner import run
|
|
33
|
+
from graph_agents_cli.deploy._image import reference_problem as image_reference_problem
|
|
34
|
+
|
|
35
|
+
DEFAULT_TAG = "latest"
|
|
36
|
+
DOCKERFILE = "Dockerfile"
|
|
37
|
+
EXIT_TOOL_FAILURE = 2
|
|
38
|
+
EXIT_CONFIG_ERROR = 3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def image_name(*, project_name: str, registry: str | None) -> str:
|
|
42
|
+
"""``<registry>/<project>`` when a registry is set, else ``<project>``."""
|
|
43
|
+
name = (project_name or "").strip()
|
|
44
|
+
if not name:
|
|
45
|
+
raise click.ClickException(
|
|
46
|
+
"The manifest has no project name; cannot derive an image name.\n"
|
|
47
|
+
" Set 'name' in graph-agents-cli-manifest.yaml."
|
|
48
|
+
)
|
|
49
|
+
reg = (registry or "").strip().rstrip("/")
|
|
50
|
+
return f"{reg}/{name}" if reg else name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_commands(
|
|
54
|
+
*, image: str, tag: str, push: bool, dockerfile: str = DOCKERFILE
|
|
55
|
+
) -> list[list[str]]:
|
|
56
|
+
ref = f"{image}:{tag}"
|
|
57
|
+
commands = [["docker", "build", "-t", ref, "-f", dockerfile, "."]]
|
|
58
|
+
if push:
|
|
59
|
+
commands.append(["docker", "push", ref])
|
|
60
|
+
return commands
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@click.command("build")
|
|
64
|
+
@click.option("--tag", default=DEFAULT_TAG, show_default=True, help="Image tag.")
|
|
65
|
+
@click.option("--registry", default=None, help="Override the manifest registry (e.g. ghcr.io/org).")
|
|
66
|
+
@click.option("--push", is_flag=True, default=False, help="Push the image after building.")
|
|
67
|
+
@click.option(
|
|
68
|
+
"--dry-run",
|
|
69
|
+
"dry_run",
|
|
70
|
+
is_flag=True,
|
|
71
|
+
default=False,
|
|
72
|
+
help="Print the docker commands without running them.",
|
|
73
|
+
)
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def cmd_build(
|
|
76
|
+
ctx: click.Context, tag: str, registry: str | None, push: bool, dry_run: bool
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Build the agent container image.
|
|
79
|
+
|
|
80
|
+
Runs `docker build -t <registry>/<name>:<tag> .` with the project's
|
|
81
|
+
runtime-specific Dockerfile. The registry defaults to the manifest's
|
|
82
|
+
`create_params.registry`; without one the image is named after the project.
|
|
83
|
+
"""
|
|
84
|
+
chdir_project_root()
|
|
85
|
+
cfg = read_project_config()
|
|
86
|
+
project_root = Path.cwd()
|
|
87
|
+
|
|
88
|
+
if not (project_root / DOCKERFILE).is_file():
|
|
89
|
+
click.secho(
|
|
90
|
+
f"Error: No {DOCKERFILE} at the project root ({project_root}).\n"
|
|
91
|
+
" Add deployment support with: graph-agents-cli scaffold enhance",
|
|
92
|
+
fg="red",
|
|
93
|
+
err=True,
|
|
94
|
+
)
|
|
95
|
+
ctx.exit(EXIT_CONFIG_ERROR)
|
|
96
|
+
|
|
97
|
+
project_name = getattr(cfg, "project_name", "") or getattr(cfg, "name", "")
|
|
98
|
+
effective_registry = registry if registry is not None else getattr(cfg, "registry", "")
|
|
99
|
+
image = image_name(project_name=project_name, registry=effective_registry)
|
|
100
|
+
# Checked before docker runs (and under --dry-run): the scaffold's placeholder
|
|
101
|
+
# registry, or any malformed reference, would otherwise surface as a docker
|
|
102
|
+
# parser error that does not name the setting to change.
|
|
103
|
+
problem = image_reference_problem(image, tag, registry=effective_registry)
|
|
104
|
+
if problem:
|
|
105
|
+
# "Error:" like every other command's configuration error.
|
|
106
|
+
click.secho(f"Error: {problem}", fg="red", err=True)
|
|
107
|
+
ctx.exit(EXIT_CONFIG_ERROR)
|
|
108
|
+
commands = build_commands(image=image, tag=tag, push=push)
|
|
109
|
+
|
|
110
|
+
if dry_run:
|
|
111
|
+
click.echo("Dry run; would execute:")
|
|
112
|
+
for cmd in commands:
|
|
113
|
+
click.echo(f" {shlex.join(cmd)}")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
_tools.require_tool(
|
|
117
|
+
"docker",
|
|
118
|
+
"Install Docker (https://docs.docker.com/get-docker/) and ensure it is in your PATH.",
|
|
119
|
+
)
|
|
120
|
+
for cmd in commands:
|
|
121
|
+
result = run(cmd, check=False)
|
|
122
|
+
if result.returncode != 0:
|
|
123
|
+
click.secho(
|
|
124
|
+
f"Error: {shlex.join(cmd[:2])} failed (exit code {result.returncode}).",
|
|
125
|
+
fg="red",
|
|
126
|
+
err=True,
|
|
127
|
+
)
|
|
128
|
+
ctx.exit(EXIT_TOOL_FAILURE)
|
|
129
|
+
click.echo(f"Built image: {image}:{tag}")
|
|
130
|
+
if push:
|
|
131
|
+
click.echo(f"Pushed image: {image}:{tag}")
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
"""graph-agents-cli install command: install project dependencies with uv."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import shutil
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
|
|
26
|
+
from graph_agents_cli import _tools
|
|
27
|
+
from graph_agents_cli._project import chdir_project_root, find_project_root
|
|
28
|
+
from graph_agents_cli._runner import run
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("install")
|
|
32
|
+
@click.option(
|
|
33
|
+
"--clean",
|
|
34
|
+
is_flag=True,
|
|
35
|
+
help="Delete and recreate the uv virtual environment (e.g. after moving the project folder).",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--locked",
|
|
39
|
+
is_flag=True,
|
|
40
|
+
help="Assert that uv.lock is up to date with pyproject.toml; fail instead of updating it.",
|
|
41
|
+
)
|
|
42
|
+
def cmd_install(clean: bool, locked: bool) -> None:
|
|
43
|
+
"""Install project dependencies."""
|
|
44
|
+
chdir_project_root()
|
|
45
|
+
# Re-materialize any missing extension working copies from their pinned
|
|
46
|
+
# SHAs before syncing: the loader tells a user with a missing copy to run
|
|
47
|
+
# this command. Never advances pins.
|
|
48
|
+
from graph_agents_cli.extension._sync import sync_extensions
|
|
49
|
+
|
|
50
|
+
sync_extensions(find_project_root(Path.cwd()))
|
|
51
|
+
install_python(clean=clean, locked=locked)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def install_python(*, clean: bool, locked: bool) -> None:
|
|
55
|
+
"""Run ``uv sync`` (``--locked`` when asked), deleting ``.venv`` first under ``--clean``."""
|
|
56
|
+
# Resolve uv up front: --clean deletes the venv. Fail quickly here (before
|
|
57
|
+
# any destructive work) rather than when calling uv.
|
|
58
|
+
_tools.require_tool("uv")
|
|
59
|
+
if clean:
|
|
60
|
+
_delete_venv()
|
|
61
|
+
cmd = ["uv", "sync"]
|
|
62
|
+
if locked:
|
|
63
|
+
cmd.append("--locked")
|
|
64
|
+
run(cmd, check_err_msg="Failed to install dependencies")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _delete_venv() -> None:
|
|
68
|
+
root = find_project_root()
|
|
69
|
+
if not root:
|
|
70
|
+
logging.warning("Could not find the project root; nothing to clean.")
|
|
71
|
+
return
|
|
72
|
+
venv_path = root / ".venv"
|
|
73
|
+
if not venv_path.exists():
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
shutil.rmtree(venv_path)
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
logging.warning("Failed to remove venv: %s", exc)
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
"""graph-agents-cli lint command: ruff, the API-policy check of every tool, the response schema."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from graph_agents_cli import _response_schema
|
|
25
|
+
from graph_agents_cli._api_policy import POLICY_FILENAME, ensure_no_legacy_api_policy
|
|
26
|
+
from graph_agents_cli._project import chdir_project_root, read_project_config
|
|
27
|
+
from graph_agents_cli._runner import run
|
|
28
|
+
from graph_agents_cli.dev.policy_check import run_policy_check
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("lint")
|
|
32
|
+
@click.option("--fix", is_flag=True, default=False, help="Auto-fix lint and formatting issues.")
|
|
33
|
+
@click.option(
|
|
34
|
+
"--policy-only",
|
|
35
|
+
"policy_only",
|
|
36
|
+
is_flag=True,
|
|
37
|
+
default=False,
|
|
38
|
+
help="Run only the API-policy check (skip ruff).",
|
|
39
|
+
)
|
|
40
|
+
def cmd_lint(fix: bool, policy_only: bool) -> None:
|
|
41
|
+
"""Run code quality checks and the API-policy check.
|
|
42
|
+
|
|
43
|
+
\b
|
|
44
|
+
ruff check . (--fix applies fixes)
|
|
45
|
+
ruff format . --check (--fix reformats in place)
|
|
46
|
+
API-policy check api-policy.yaml must pass the strict schema, and
|
|
47
|
+
every API_CALLS entry of every tool must name a
|
|
48
|
+
declared API and be allowed by its rules (and
|
|
49
|
+
exist in its OpenAPI spec when one is named)
|
|
50
|
+
Response schema <agent directory>/response_schema.json, when the
|
|
51
|
+
project has one (structured final answers), must
|
|
52
|
+
be a schema the agent starts with; a warning when
|
|
53
|
+
agent.py does not pass response_format() to the
|
|
54
|
+
agent or has no StructuredAnswer() in its
|
|
55
|
+
middleware
|
|
56
|
+
|
|
57
|
+
\b
|
|
58
|
+
Exit codes:
|
|
59
|
+
0 clean
|
|
60
|
+
1 a refused call or an unreadable API_CALLS, or ruff failed
|
|
61
|
+
3 configuration error: an invalid api-policy.yaml or response
|
|
62
|
+
schema, the retired product policy, or not in a project
|
|
63
|
+
"""
|
|
64
|
+
chdir_project_root()
|
|
65
|
+
cfg = read_project_config()
|
|
66
|
+
ensure_no_legacy_api_policy(Path.cwd())
|
|
67
|
+
check_response_schema(Path.cwd(), cfg.agent_directory)
|
|
68
|
+
if not policy_only:
|
|
69
|
+
lint_python(fix=fix)
|
|
70
|
+
violations = run_policy_check(
|
|
71
|
+
Path.cwd(),
|
|
72
|
+
cfg.agent_directory,
|
|
73
|
+
policy_file=cfg.api_policy_file or POLICY_FILENAME,
|
|
74
|
+
runtime=cfg.runtime,
|
|
75
|
+
policy_declared=bool(cfg.api_policy_file),
|
|
76
|
+
auth_policy=cfg.auth_policy,
|
|
77
|
+
)
|
|
78
|
+
if violations:
|
|
79
|
+
raise click.ClickException(
|
|
80
|
+
f"API policy check failed: {violations} violation(s). Fix the tool declarations, "
|
|
81
|
+
"or change api-policy.yaml with `graph-agents-cli api` (the commands above) in a "
|
|
82
|
+
"reviewed pull request."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_response_schema(project_root: Path, agent_directory: str) -> None:
|
|
87
|
+
"""The project's response schema, when it has one: refused (exit 3) unless the agent starts
|
|
88
|
+
with it; a warning when ``agent.py`` does not build the agent with ``response_format()`` or
|
|
89
|
+
lacks ``StructuredAnswer()`` in its middleware."""
|
|
90
|
+
path = _response_schema.schema_file(project_root, agent_directory)
|
|
91
|
+
if not path.is_file():
|
|
92
|
+
return
|
|
93
|
+
_response_schema.load_schema(path)
|
|
94
|
+
problems = _response_schema.wiring_problems(project_root, agent_directory)
|
|
95
|
+
if problems:
|
|
96
|
+
click.secho(
|
|
97
|
+
f"Warning: {agent_directory}/{_response_schema.SCHEMA_FILENAME} declares structured "
|
|
98
|
+
f"answers, but {'; and '.join(problems)}. Pass "
|
|
99
|
+
"response_format=response_format(model, tools) to create_agent and put "
|
|
100
|
+
"StructuredAnswer() last in middleware(); see the template's "
|
|
101
|
+
"app_utils/structured.py.",
|
|
102
|
+
fg="yellow",
|
|
103
|
+
err=True,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
click.echo(f"Response schema: {agent_directory}/{_response_schema.SCHEMA_FILENAME} OK")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def lint_python(*, fix: bool) -> None:
|
|
110
|
+
"""Run ``ruff check`` and ``ruff format`` in the project."""
|
|
111
|
+
if fix:
|
|
112
|
+
run(["uv", "run", "ruff", "check", ".", "--fix"], check_err_msg="Ruff check --fix failed")
|
|
113
|
+
run(["uv", "run", "ruff", "format", "."], check_err_msg="Ruff format failed")
|
|
114
|
+
else:
|
|
115
|
+
run(["uv", "run", "ruff", "check", "."], check_err_msg="Ruff check failed")
|
|
116
|
+
run(
|
|
117
|
+
["uv", "run", "ruff", "format", ".", "--check"],
|
|
118
|
+
check_err_msg="Ruff format check failed",
|
|
119
|
+
)
|