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,253 @@
|
|
|
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
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Parse and validate graph-agents-cli-extension.yaml (schema: graph-agents-cli-extension/v1alpha1)."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import difflib
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path, PurePosixPath
|
|
25
|
+
from types import UnionType
|
|
26
|
+
from typing import Annotated, Any, Literal, Union, get_args, get_origin
|
|
27
|
+
|
|
28
|
+
from pydantic import (
|
|
29
|
+
BaseModel,
|
|
30
|
+
BeforeValidator,
|
|
31
|
+
ConfigDict,
|
|
32
|
+
Field,
|
|
33
|
+
ValidationError,
|
|
34
|
+
model_validator,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from graph_agents_cli.extension._compat import validate_range
|
|
38
|
+
|
|
39
|
+
# The manifest format version, tracked separately from the graph-agents-cli version.
|
|
40
|
+
# A second format would widen `ExtensionManifest.schema_version` to a Union.
|
|
41
|
+
# Alpha while the extension system is experimental: the format may still change
|
|
42
|
+
# in a breaking way, and `v1` is reserved for the point it stops doing so.
|
|
43
|
+
V1ALPHA1 = "graph-agents-cli-extension/v1alpha1"
|
|
44
|
+
EXTENSION_FILE = "graph-agents-cli-extension.yaml"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExtensionSpecError(Exception):
|
|
48
|
+
"""Raised on structural errors in an extension spec."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- Wire models -------------------------------------------------------------
|
|
52
|
+
# These mirror graph-agents-cli-extension.yaml and generate the published JSON Schema.
|
|
53
|
+
# `extra="forbid"` so a typo like `reqires:` is an error rather than a section
|
|
54
|
+
# that silently does nothing.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _reject_bool(value: Any) -> Any:
|
|
58
|
+
"""YAML 1.1 reads `yes`/`on` as True, which is not a number in JSON Schema."""
|
|
59
|
+
if isinstance(value, bool):
|
|
60
|
+
raise ValueError('expected a version range, e.g. ">=1.3,<2"')
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Strict(BaseModel):
|
|
65
|
+
model_config = ConfigDict(extra="forbid")
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="before")
|
|
68
|
+
@classmethod
|
|
69
|
+
def _empty_key_means_absent(cls, data: Any) -> Any:
|
|
70
|
+
"""`commands:` with nothing under it is how authors comment a section out.
|
|
71
|
+
|
|
72
|
+
Only for keys this model knows: dropping unknown ones would hide the
|
|
73
|
+
typo that `extra="forbid"` exists to catch.
|
|
74
|
+
"""
|
|
75
|
+
if not isinstance(data, dict):
|
|
76
|
+
return data
|
|
77
|
+
known = {f.alias or n for n, f in cls.model_fields.items()}
|
|
78
|
+
return {k: v for k, v in data.items() if v is not None or k not in known}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CommandEntry(_Strict):
|
|
82
|
+
run: list[str] = Field(min_length=1)
|
|
83
|
+
description: str = ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Commands(_Strict):
|
|
87
|
+
override: dict[str, CommandEntry] = Field(default_factory=dict)
|
|
88
|
+
add: dict[str, CommandEntry] = Field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Requires(_Strict):
|
|
92
|
+
# `agents_cli: 1.2` is natural YAML and arrives as a number; the range is
|
|
93
|
+
# validated semantically below, and a bad one is dropped rather than fatal.
|
|
94
|
+
agents_cli: Annotated[str | float | None, BeforeValidator(_reject_bool)] = None
|
|
95
|
+
on_incompatible: Literal["warn", "error"] = "warn"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ExtensionManifest(_Strict):
|
|
99
|
+
"""`graph-agents-cli-extension.yaml` as written on disk."""
|
|
100
|
+
|
|
101
|
+
# This is the v1alpha1 document, so it accepts only v1alpha1: an editor
|
|
102
|
+
# validating against it has to agree with the loader, which rejects
|
|
103
|
+
# anything else.
|
|
104
|
+
schema_version: Literal["graph-agents-cli-extension/v1alpha1"] = Field(
|
|
105
|
+
default=V1ALPHA1, alias="schema"
|
|
106
|
+
)
|
|
107
|
+
name: str | None = Field(default=None, min_length=1)
|
|
108
|
+
description: str = ""
|
|
109
|
+
requires: Requires | None = None
|
|
110
|
+
commands: Commands = Field(default_factory=Commands)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _describe(error: Mapping[str, Any]) -> str:
|
|
114
|
+
"""One validation error as a line an extension author can act on."""
|
|
115
|
+
loc = ".".join(str(part) for part in error["loc"] if part != "[key]")
|
|
116
|
+
if error["type"] == "extra_forbidden":
|
|
117
|
+
key = str(error["loc"][-1])
|
|
118
|
+
near = difflib.get_close_matches(key, _known_keys(error["loc"][:-1]), n=1)
|
|
119
|
+
hint = f" (did you mean {near[0]!r}?)" if near else ""
|
|
120
|
+
return f"unknown key {loc!r}{hint}"
|
|
121
|
+
# Model class names mean nothing to someone editing YAML.
|
|
122
|
+
msg = re.sub(r" or instance of \w+", "", error["msg"])
|
|
123
|
+
return f"{loc or '(root)'}: {msg}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _known_keys(loc: tuple[Any, ...]) -> list[str]:
|
|
127
|
+
"""Field names of the manifest model at `loc`, for did-you-mean hints.
|
|
128
|
+
|
|
129
|
+
A container's key or index consumes a `loc` part without changing the model,
|
|
130
|
+
so `commands.add.<name>.<typo>` still resolves to CommandEntry's fields.
|
|
131
|
+
"""
|
|
132
|
+
current: Any = ExtensionManifest
|
|
133
|
+
for part in loc:
|
|
134
|
+
fields = getattr(current, "model_fields", {})
|
|
135
|
+
field = fields.get(str(part)) or next(
|
|
136
|
+
(f for f in fields.values() if f.alias == str(part)), None
|
|
137
|
+
)
|
|
138
|
+
if field is None:
|
|
139
|
+
# This part is a dict key or a list index, not a field name.
|
|
140
|
+
nested = _model_in(current)
|
|
141
|
+
if nested is None:
|
|
142
|
+
return []
|
|
143
|
+
current = nested
|
|
144
|
+
continue
|
|
145
|
+
current = _unwrap_optional(field.annotation)
|
|
146
|
+
return [f.alias or n for n, f in getattr(current, "model_fields", {}).items()]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _unwrap_optional(annotation: Any) -> Any:
|
|
150
|
+
"""`X | None` -> X, leaving `dict[str, X]` and `list[X]` for the key/index step."""
|
|
151
|
+
if get_origin(annotation) not in (Union, UnionType):
|
|
152
|
+
return annotation
|
|
153
|
+
args = [a for a in get_args(annotation) if a is not type(None)]
|
|
154
|
+
return args[0] if len(args) == 1 else annotation
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _model_in(annotation: Any) -> Any:
|
|
158
|
+
"""The model a container holds, e.g. dict[str, CommandEntry] -> CommandEntry."""
|
|
159
|
+
for arg in getattr(annotation, "__args__", ()):
|
|
160
|
+
if isinstance(arg, type) and issubclass(arg, BaseModel):
|
|
161
|
+
return arg
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class ExtensionCommand:
|
|
167
|
+
name: str # dotted command path, e.g. "deploy" or "eval.generate"
|
|
168
|
+
run: tuple[str, ...]
|
|
169
|
+
description: str
|
|
170
|
+
kind: str # "override" | "add"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ExtensionSpec:
|
|
175
|
+
name: str
|
|
176
|
+
description: str
|
|
177
|
+
root: Path
|
|
178
|
+
commands: tuple[ExtensionCommand, ...]
|
|
179
|
+
# Install scope ("user" | "project"), stamped by the loader at discovery —
|
|
180
|
+
# it comes from where the extension was found, not from the manifest.
|
|
181
|
+
scope: str = ""
|
|
182
|
+
# Optional author-declared graph-agents-cli compatibility range (e.g. ">=1.1,<2").
|
|
183
|
+
requires_agents_cli: str | None = None
|
|
184
|
+
# The manifest's "warn" | "error", resolved at parse time. Only two states
|
|
185
|
+
# exist and the manifest model already validates them, so the rest of the
|
|
186
|
+
# CLI reads a bool rather than re-comparing strings.
|
|
187
|
+
error_on_incompatible: bool = False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _to_command(name: str, entry: CommandEntry, kind: str) -> ExtensionCommand:
|
|
191
|
+
_warn_if_script_entrypoint(name, kind, entry.run[0])
|
|
192
|
+
return ExtensionCommand(
|
|
193
|
+
name=name, run=tuple(entry.run), description=entry.description, kind=kind
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Suffixes that are run by an interpreter, not executed directly on Windows.
|
|
198
|
+
_SCRIPT_SUFFIXES = frozenset({".py", ".sh", ".js", ".ts", ".rb", ".pl"})
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _warn_if_script_entrypoint(name: str, kind: str, first_token: str) -> None:
|
|
202
|
+
"""Executed without a shell: a bare script needs +x and a shebang on POSIX, and never runs on Windows."""
|
|
203
|
+
if PurePosixPath(first_token).suffix in _SCRIPT_SUFFIXES:
|
|
204
|
+
logging.warning(
|
|
205
|
+
"commands.%s.%s: `run` starts with the script %r; prefix an "
|
|
206
|
+
'interpreter (e.g. ["uv", "run", "python", %r]) so it also '
|
|
207
|
+
"runs on Windows.",
|
|
208
|
+
kind,
|
|
209
|
+
name,
|
|
210
|
+
first_token,
|
|
211
|
+
first_token,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def parse_extension_spec(data: dict[str, Any], *, root: Path, default_name: str) -> ExtensionSpec:
|
|
216
|
+
"""Raises ExtensionSpecError on any structural problem; the caller decides whether that is fatal."""
|
|
217
|
+
try:
|
|
218
|
+
manifest = ExtensionManifest.model_validate(data)
|
|
219
|
+
except ValidationError as e:
|
|
220
|
+
problems = "\n - ".join(_describe(err) for err in e.errors())
|
|
221
|
+
if len(e.errors()) > 1:
|
|
222
|
+
problems = f"\n - {problems}"
|
|
223
|
+
raise ExtensionSpecError(problems) from e
|
|
224
|
+
|
|
225
|
+
commands = [
|
|
226
|
+
_to_command(name, entry, kind)
|
|
227
|
+
for kind in ("override", "add")
|
|
228
|
+
for name, entry in getattr(manifest.commands, kind).items()
|
|
229
|
+
]
|
|
230
|
+
requires_agents_cli, error_on_incompatible = _parse_requires(manifest.requires)
|
|
231
|
+
return ExtensionSpec(
|
|
232
|
+
name=manifest.name or default_name,
|
|
233
|
+
description=manifest.description,
|
|
234
|
+
root=root,
|
|
235
|
+
commands=tuple(commands),
|
|
236
|
+
requires_agents_cli=requires_agents_cli,
|
|
237
|
+
error_on_incompatible=error_on_incompatible,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_requires(requires: Requires | None) -> tuple[str | None, bool]:
|
|
242
|
+
"""An unparseable range is warned about and dropped: a bad compat hint must not make an otherwise-valid extension unloadable."""
|
|
243
|
+
if requires is None:
|
|
244
|
+
return None, False
|
|
245
|
+
raw_range = None if requires.agents_cli is None else str(requires.agents_cli)
|
|
246
|
+
if raw_range is not None and not validate_range(raw_range):
|
|
247
|
+
logging.warning(
|
|
248
|
+
"requires.agents_cli %r is not a valid version range; ignoring the "
|
|
249
|
+
"range (the extension still loads).",
|
|
250
|
+
raw_range,
|
|
251
|
+
)
|
|
252
|
+
raw_range = None
|
|
253
|
+
return raw_range, requires.on_incompatible == "error"
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Re-materialize vendored extension working copies from pinned SHAs."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from graph_agents_cli.__init__ import __version__
|
|
25
|
+
from graph_agents_cli.extension._loader import incompatible_spec
|
|
26
|
+
from graph_agents_cli.extension._manifest import (
|
|
27
|
+
EXTENSIONS_FILE,
|
|
28
|
+
read_extension_entries,
|
|
29
|
+
)
|
|
30
|
+
from graph_agents_cli.extension._paths import user_config_root, vendored_extensions_dir
|
|
31
|
+
from graph_agents_cli.extension._refs import RefParseError, anchor_local, parse_ref
|
|
32
|
+
from graph_agents_cli.extension._resolver import (
|
|
33
|
+
ResolverError,
|
|
34
|
+
expected_stamp,
|
|
35
|
+
materialize,
|
|
36
|
+
read_stamp,
|
|
37
|
+
)
|
|
38
|
+
from graph_agents_cli.extension._spec import ExtensionSpecError
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _scope_roots(project_root: Path | None) -> list[Path]:
|
|
42
|
+
"""Roots to sync, project first. The scope label is not needed: both scopes
|
|
43
|
+
store the list under the same filename."""
|
|
44
|
+
roots = [] if project_root is None else [project_root]
|
|
45
|
+
roots.append(user_config_root())
|
|
46
|
+
return roots
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sync_extensions(project_root: Path | None) -> list[str]:
|
|
50
|
+
"""Ensure every recorded extension is vendored on disk.
|
|
51
|
+
|
|
52
|
+
Reports what it restored and returns those names.
|
|
53
|
+
"""
|
|
54
|
+
restored: list[str] = []
|
|
55
|
+
for scope_root in _scope_roots(project_root):
|
|
56
|
+
manifest = scope_root / EXTENSIONS_FILE
|
|
57
|
+
for entry in read_extension_entries(manifest):
|
|
58
|
+
extension_dir = vendored_extensions_dir(scope_root) / entry.name
|
|
59
|
+
try:
|
|
60
|
+
ref = anchor_local(parse_ref(entry.source, ref_override=entry.sha), scope_root)
|
|
61
|
+
# Re-materialize when the copy is missing OR no longer matches
|
|
62
|
+
# its source: a bumped pin, a partial copy, or — for a local
|
|
63
|
+
# path, whose source is a working tree rather than a commit —
|
|
64
|
+
# any edit made to it since the copy was taken.
|
|
65
|
+
if extension_dir.exists() and read_stamp(extension_dir) == expected_stamp(
|
|
66
|
+
ref, entry.sha
|
|
67
|
+
):
|
|
68
|
+
continue
|
|
69
|
+
# name=entry.name pins the vendored dir to the stable id (see
|
|
70
|
+
# materialize) so a restore survives an upstream rename.
|
|
71
|
+
materialize(
|
|
72
|
+
ref,
|
|
73
|
+
entry.sha,
|
|
74
|
+
vendored_extensions_dir(scope_root),
|
|
75
|
+
name=entry.name,
|
|
76
|
+
)
|
|
77
|
+
restored.append(entry.name)
|
|
78
|
+
# Never block a restore (that would brick `install`); just warn
|
|
79
|
+
# if the restored pin is out of range for the running CLI.
|
|
80
|
+
try:
|
|
81
|
+
stale = incompatible_spec(extension_dir)
|
|
82
|
+
except ExtensionSpecError as e:
|
|
83
|
+
# Restore must never fail: it is how a bad pin gets fixed.
|
|
84
|
+
logging.warning("%s: %s.", extension_dir, e)
|
|
85
|
+
stale = None
|
|
86
|
+
if stale is not None:
|
|
87
|
+
logging.warning(
|
|
88
|
+
"Restored extension %r requires graph-agents-cli %s but running %s.",
|
|
89
|
+
entry.name,
|
|
90
|
+
stale.requires_agents_cli,
|
|
91
|
+
__version__,
|
|
92
|
+
)
|
|
93
|
+
except (ResolverError, RefParseError) as e:
|
|
94
|
+
logging.warning(
|
|
95
|
+
"Could not restore extension %r from %s: %s",
|
|
96
|
+
entry.name,
|
|
97
|
+
entry.source,
|
|
98
|
+
e,
|
|
99
|
+
)
|
|
100
|
+
if restored:
|
|
101
|
+
click.echo(f"Restored extensions: {', '.join(restored)}")
|
|
102
|
+
return restored
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
"""Consent gate for running third-party extension code."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from graph_agents_cli.extension._refs import ExtensionRef
|
|
25
|
+
|
|
26
|
+
NO_TERMINAL_HINT = (
|
|
27
|
+
" Not asking: stdin is not a terminal. Pass -y to trust this extension non-interactively."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stdin_is_interactive() -> bool:
|
|
32
|
+
"""True when a person can answer a prompt (stdin is a terminal)."""
|
|
33
|
+
try:
|
|
34
|
+
return sys.stdin is not None and sys.stdin.isatty()
|
|
35
|
+
except (AttributeError, ValueError): # replaced or closed stdin
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def confirm_trust(ref: ExtensionRef, *, auto_approve: bool) -> bool:
|
|
40
|
+
"""Return True if it's OK to install/run this extension's code.
|
|
41
|
+
|
|
42
|
+
First-party extensions are trusted. Everything else prompts unless
|
|
43
|
+
`auto_approve`. Without a terminal to answer on (CI, a pipe, `< /dev/null`)
|
|
44
|
+
it does not prompt, which would abort or block: it says to pass -y and
|
|
45
|
+
returns False.
|
|
46
|
+
"""
|
|
47
|
+
if ref.kind == "first_party" or auto_approve:
|
|
48
|
+
return True
|
|
49
|
+
click.echo()
|
|
50
|
+
click.secho(
|
|
51
|
+
f" Extension source {ref.raw!r} is third-party. It can run arbitrary code "
|
|
52
|
+
"on your machine when its commands are invoked.",
|
|
53
|
+
fg="yellow",
|
|
54
|
+
)
|
|
55
|
+
if not stdin_is_interactive():
|
|
56
|
+
click.secho(NO_TERMINAL_HINT, fg="yellow")
|
|
57
|
+
return False
|
|
58
|
+
return click.confirm(" Install and trust this extension?", default=False)
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""graph-agents-cli extension add command."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
|
|
26
|
+
from graph_agents_cli.__init__ import __version__
|
|
27
|
+
from graph_agents_cli._project import NotInProjectError, find_project_root
|
|
28
|
+
from graph_agents_cli._trust import require_confirmation
|
|
29
|
+
from graph_agents_cli.extension._compat import is_compatible
|
|
30
|
+
from graph_agents_cli.extension._loader import (
|
|
31
|
+
RECOVERY_COMMANDS,
|
|
32
|
+
load_extension_set,
|
|
33
|
+
load_extension_spec,
|
|
34
|
+
)
|
|
35
|
+
from graph_agents_cli.extension._manifest import (
|
|
36
|
+
EXTENSIONS_FILE,
|
|
37
|
+
ExtensionEntry,
|
|
38
|
+
read_extension_entries,
|
|
39
|
+
upsert_extension_entry,
|
|
40
|
+
)
|
|
41
|
+
from graph_agents_cli.extension._paths import (
|
|
42
|
+
scope_root,
|
|
43
|
+
user_config_root,
|
|
44
|
+
vendored_extensions_dir,
|
|
45
|
+
)
|
|
46
|
+
from graph_agents_cli.extension._refs import (
|
|
47
|
+
ExtensionRef,
|
|
48
|
+
RefParseError,
|
|
49
|
+
anchor_local,
|
|
50
|
+
parse_ref,
|
|
51
|
+
)
|
|
52
|
+
from graph_agents_cli.extension._resolver import ResolverError, materialize, resolve_sha
|
|
53
|
+
from graph_agents_cli.extension._spec import (
|
|
54
|
+
EXTENSION_FILE,
|
|
55
|
+
ExtensionSpec,
|
|
56
|
+
ExtensionSpecError,
|
|
57
|
+
)
|
|
58
|
+
from graph_agents_cli.extension._trust import confirm_trust
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _recorded_local_source(ref: ExtensionRef, root: Path, scope: str) -> str:
|
|
62
|
+
"""The ``local@`` source to record, resolvable from the scope root later.
|
|
63
|
+
|
|
64
|
+
``install`` and ``update`` resolve a relative local source against the scope
|
|
65
|
+
root, so the path typed here (relative to wherever the command ran) is
|
|
66
|
+
rewritten: relative to the project root for project scope, which keeps the
|
|
67
|
+
committed entry portable, absolute for user scope.
|
|
68
|
+
"""
|
|
69
|
+
absolute = (ref.local_path or Path.cwd()).resolve()
|
|
70
|
+
path = absolute.as_posix()
|
|
71
|
+
if scope == "project":
|
|
72
|
+
try:
|
|
73
|
+
path = Path(os.path.relpath(absolute, root.resolve())).as_posix()
|
|
74
|
+
except ValueError: # another drive on Windows: only an absolute path works
|
|
75
|
+
pass
|
|
76
|
+
selector = f"#{ref.selector}" if ref.selector else ""
|
|
77
|
+
return f"local@{path}{selector}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_hint(reference: str, ref: ExtensionRef) -> str:
|
|
81
|
+
"""A pointer to ``local@`` when a repo shorthand also names a local directory."""
|
|
82
|
+
if ref.kind in ("github", "first_party") and Path(reference.split("#", 1)[0]).is_dir():
|
|
83
|
+
return (
|
|
84
|
+
f"\n {reference!r} is also a local directory: to install it from disk, use "
|
|
85
|
+
f"local@{reference} (or ./{reference})."
|
|
86
|
+
)
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_or_fail(extension_dir: Path) -> ExtensionSpec | None:
|
|
91
|
+
"""Parse the vendored manifest, turning a bad one into a failed install.
|
|
92
|
+
|
|
93
|
+
Parsed once here and handed to both gates, so a manifest is not read twice
|
|
94
|
+
and the two cannot disagree about what it says.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
return load_extension_spec(extension_dir)
|
|
98
|
+
except ExtensionSpecError as e:
|
|
99
|
+
raise click.ClickException(
|
|
100
|
+
f"{EXTENSION_FILE} is not valid: {e}\nNothing was installed."
|
|
101
|
+
) from e
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _check_compat(spec: ExtensionSpec | None) -> None:
|
|
105
|
+
"""Gate a freshly vendored extension against the running CLI version.
|
|
106
|
+
|
|
107
|
+
`error` mode raises so the install fails; `warn` mode logs and proceeds.
|
|
108
|
+
This is the deliberate install gate, the only place an incompatibility
|
|
109
|
+
hard-fails; at runtime it only ever warns.
|
|
110
|
+
"""
|
|
111
|
+
if (
|
|
112
|
+
spec is None
|
|
113
|
+
or not spec.requires_agents_cli
|
|
114
|
+
or is_compatible(__version__, spec.requires_agents_cli)
|
|
115
|
+
):
|
|
116
|
+
return
|
|
117
|
+
msg = (
|
|
118
|
+
f"Extension requires graph-agents-cli {spec.requires_agents_cli} but you're "
|
|
119
|
+
f"running {__version__}."
|
|
120
|
+
)
|
|
121
|
+
if spec.error_on_incompatible:
|
|
122
|
+
raise click.ClickException(
|
|
123
|
+
f"{msg} Not installing (on_incompatible: error). "
|
|
124
|
+
"Pin a compatible ref with --ref, or upgrade the CLI."
|
|
125
|
+
)
|
|
126
|
+
logging.warning("%s Installing anyway (on_incompatible: warn).", msg)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _check_reserved_commands(spec: ExtensionSpec | None) -> None:
|
|
130
|
+
"""Refuse an extension that claims a command used to repair extensions.
|
|
131
|
+
|
|
132
|
+
Caught at install because that is where a human can read why. The loader
|
|
133
|
+
ignores such a claim too, for manifests edited by hand.
|
|
134
|
+
"""
|
|
135
|
+
if spec is None:
|
|
136
|
+
return
|
|
137
|
+
reserved = sorted({c.name for c in spec.commands} & RECOVERY_COMMANDS)
|
|
138
|
+
if reserved:
|
|
139
|
+
raise click.ClickException(
|
|
140
|
+
f"Extension claims {', '.join(repr(r) for r in reserved)}, which "
|
|
141
|
+
"cannot be overridden: these are how you repair or remove an "
|
|
142
|
+
"extension, so an extension owning them could own its own uninstall."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _check_command_conflicts(
|
|
147
|
+
project_root: Path | None, spec: ExtensionSpec | None, scope: str
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Raise if the candidate claims a command *another* extension already owns.
|
|
150
|
+
|
|
151
|
+
Re-adding the same name at the same scope is a replace, which is how a pin
|
|
152
|
+
moves, so the candidate must not be read as conflicting with the copy it is
|
|
153
|
+
about to overwrite.
|
|
154
|
+
"""
|
|
155
|
+
if spec is None:
|
|
156
|
+
return
|
|
157
|
+
installed = load_extension_set(project_root, user_config_root())
|
|
158
|
+
clashes = sorted(
|
|
159
|
+
(contribution.name, owner.extension_name)
|
|
160
|
+
for contribution in spec.commands
|
|
161
|
+
if (owner := installed.commands.get(contribution.name)) is not None
|
|
162
|
+
and owner.scope == scope
|
|
163
|
+
and owner.extension_name != spec.name
|
|
164
|
+
)
|
|
165
|
+
if clashes:
|
|
166
|
+
raise click.ClickException(
|
|
167
|
+
"Command conflict(s) with an already-installed extension: "
|
|
168
|
+
+ ", ".join(f"{cmd} (owned by {owner!r})" for cmd, owner in clashes)
|
|
169
|
+
+ ". Remove or rename the conflicting extension."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@click.command("add")
|
|
174
|
+
@click.argument("reference")
|
|
175
|
+
@click.option(
|
|
176
|
+
"--global",
|
|
177
|
+
"global_",
|
|
178
|
+
is_flag=True,
|
|
179
|
+
default=False,
|
|
180
|
+
help=(
|
|
181
|
+
"Install for all projects (user scope, global/org-wide). Default is "
|
|
182
|
+
"project scope (committed to this repo only), so the extension cannot "
|
|
183
|
+
"affect your other projects."
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
@click.option("--ref", "ref_override", default=None, help="Pin a branch, tag, or SHA.")
|
|
187
|
+
@require_confirmation("Install this extension?")
|
|
188
|
+
def cmd_add(
|
|
189
|
+
reference: str,
|
|
190
|
+
global_: bool,
|
|
191
|
+
ref_override: str | None,
|
|
192
|
+
auto_approve: bool,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Add an extension from a git reference or local path."""
|
|
195
|
+
scope = "user" if global_ else "project"
|
|
196
|
+
try:
|
|
197
|
+
ref = parse_ref(reference, ref_override=ref_override)
|
|
198
|
+
except RefParseError as e:
|
|
199
|
+
raise click.ClickException(str(e)) from e
|
|
200
|
+
|
|
201
|
+
root = scope_root(scope)
|
|
202
|
+
if root is None:
|
|
203
|
+
# Checked before the trust prompt: nothing to confirm when it cannot install.
|
|
204
|
+
raise NotInProjectError(
|
|
205
|
+
"No graph-agents-cli-manifest.yaml found. Run inside a project (extensions "
|
|
206
|
+
"install at project scope by default), or pass --global to install "
|
|
207
|
+
"for all projects."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if not confirm_trust(ref, auto_approve=auto_approve):
|
|
211
|
+
raise click.ClickException("Aborted: extension not trusted.")
|
|
212
|
+
|
|
213
|
+
project_root = root if scope == "project" else find_project_root(Path.cwd())
|
|
214
|
+
|
|
215
|
+
source = ref.raw
|
|
216
|
+
if ref.kind == "local":
|
|
217
|
+
# Resolved against the current directory now; recorded against the root.
|
|
218
|
+
ref = anchor_local(ref, Path.cwd())
|
|
219
|
+
source = _recorded_local_source(ref, root, scope)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
sha = resolve_sha(ref)
|
|
223
|
+
name, extension_dir = materialize(ref, sha, vendored_extensions_dir(root))
|
|
224
|
+
except ResolverError as e:
|
|
225
|
+
error = click.ClickException(f"{e}{_resolve_hint(reference, ref)}")
|
|
226
|
+
# A local path that is not an extension is a configuration error (3);
|
|
227
|
+
# git or the network failing to resolve a repo is a tool failure (2).
|
|
228
|
+
error.exit_code = 3 if ref.kind == "local" else 2
|
|
229
|
+
raise error from e
|
|
230
|
+
|
|
231
|
+
# Every gate runs before anything is recorded, so a refused install leaves
|
|
232
|
+
# no entry to roll back. The vendored copy has to exist first (the gates read
|
|
233
|
+
# its manifest), so that is the only thing to clean up.
|
|
234
|
+
try:
|
|
235
|
+
spec = _parse_or_fail(extension_dir)
|
|
236
|
+
_check_compat(spec)
|
|
237
|
+
_check_reserved_commands(spec)
|
|
238
|
+
_check_command_conflicts(project_root, spec, scope)
|
|
239
|
+
except click.ClickException as e:
|
|
240
|
+
if extension_dir.exists():
|
|
241
|
+
shutil.rmtree(extension_dir)
|
|
242
|
+
# Removing the copy of an extension that is still recorded leaves the
|
|
243
|
+
# entry pointing at nothing, and its commands silently fall back to the
|
|
244
|
+
# built-ins until the copy is back.
|
|
245
|
+
if any(entry.name == name for entry in read_extension_entries(root / EXTENSIONS_FILE)):
|
|
246
|
+
raise click.ClickException(
|
|
247
|
+
f"{e.format_message()}\n"
|
|
248
|
+
f" {name!r} is still recorded; restore its copy with: graph-agents-cli install"
|
|
249
|
+
) from e
|
|
250
|
+
raise
|
|
251
|
+
|
|
252
|
+
entry = ExtensionEntry(name=name, source=source, ref=ref.ref or "HEAD", sha=sha, scope=scope)
|
|
253
|
+
upsert_extension_entry(root / EXTENSIONS_FILE, entry)
|
|
254
|
+
|
|
255
|
+
click.secho(f"Added extension {name!r} ({scope} scope) from {source}.", fg="green")
|
|
256
|
+
click.secho(
|
|
257
|
+
"Extensions are experimental; the manifest format may still change.",
|
|
258
|
+
dim=True,
|
|
259
|
+
)
|