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,188 @@
|
|
|
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
|
+
"""Which allow-listed Secret keys an environment cannot run without.
|
|
15
|
+
|
|
16
|
+
A missing key only shows up once the pods start: a crash loop (no provider key,
|
|
17
|
+
no database URI) or every request answered 503 (no ``API_KEY`` under
|
|
18
|
+
``shared-bearer``). Outside dev the chart requires the Secret itself
|
|
19
|
+
(``secretOptional: false``), so a missing Secret keeps the pods from starting,
|
|
20
|
+
but it cannot tell which keys the Secret holds.
|
|
21
|
+
``deploy`` checks the live Secret against this list before ``helm upgrade``,
|
|
22
|
+
and ``secrets status`` exits 1 only when one of these is missing, so both work
|
|
23
|
+
as gates while optional keys (judge key, LangSmith key, API tokens) stay
|
|
24
|
+
optional.
|
|
25
|
+
|
|
26
|
+
The list follows the environment's merged chart values, the same inputs the pod
|
|
27
|
+
gets: ``env.MODEL_PROVIDER`` (the manifest's provider when unset) needs its
|
|
28
|
+
key, except ``openai-compatible`` servers (often keyless on-network) and the
|
|
29
|
+
``fake`` test model; ``env.AUTH_POLICY`` ``shared-bearer`` needs ``API_KEY``;
|
|
30
|
+
the database and Redis URIs are needed unless the bundled subchart provides
|
|
31
|
+
them (``postgresql.enabled`` / ``redis.enabled``) or ``env.CHECKPOINTER`` is
|
|
32
|
+
``memory``. A key set as a plain chart ``env`` value does not need the Secret.
|
|
33
|
+
Only keys in the manifest's ``secrets.keys`` are ever required; removing one
|
|
34
|
+
from the allow-list is how a deployment opts out.
|
|
35
|
+
|
|
36
|
+
One key joins the allow-list without being listed there: ``AUTH_JWT_SECRET``,
|
|
37
|
+
the shared secret of the ``jwt`` policy's HS* algorithms. A project opts into
|
|
38
|
+
HS* with ``AUTH_JWT_ALLOW_HS=true`` / ``AUTH_JWT_ALGORITHMS=HS256`` in the
|
|
39
|
+
environment's chart values or its env file; the secret itself must then reach
|
|
40
|
+
the pods through the Secret (never the ConfigMap). It is required when the
|
|
41
|
+
chart values list an HS* algorithm: the pod refuses to start without it.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import dataclasses
|
|
47
|
+
from collections.abc import Mapping
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from graph_agents_cli._defaults import PROVIDER_KEY_VARS, normalize_auth_policy
|
|
51
|
+
from graph_agents_cli._output import Console
|
|
52
|
+
from graph_agents_cli.deploy._config import DeploySettings
|
|
53
|
+
|
|
54
|
+
# Providers whose key is optional: on-network OpenAI-compatible servers are often
|
|
55
|
+
# keyless, and the fake model needs none.
|
|
56
|
+
KEYLESS_PROVIDERS = frozenset({"openai-compatible", "fake"})
|
|
57
|
+
|
|
58
|
+
JWT_SECRET_KEY = "AUTH_JWT_SECRET"
|
|
59
|
+
# The HS* settings the app reads (see the template's app/app_utils/auth.py).
|
|
60
|
+
JWT_HS_SETTINGS = ("AUTH_JWT_ALLOW_HS", "AUTH_JWT_ALGORITHMS")
|
|
61
|
+
_HMAC_ALGORITHMS = frozenset({"HS256", "HS384", "HS512"})
|
|
62
|
+
_TRUE = ("1", "true", "yes", "on")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _env_value(values: dict[str, Any], key: str) -> str:
|
|
66
|
+
env = values.get("env") if isinstance(values.get("env"), dict) else {}
|
|
67
|
+
value = env.get(key)
|
|
68
|
+
return "" if value is None else str(value).strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _auth_policy(settings: DeploySettings, values: dict[str, Any]) -> str:
|
|
72
|
+
return normalize_auth_policy(
|
|
73
|
+
_env_value(values, "AUTH_POLICY") or settings.auth_policy, warn=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _lists_hmac(algorithms: str) -> bool:
|
|
78
|
+
return any(a.strip().upper() in _HMAC_ALGORITHMS for a in algorithms.split(","))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _hs_settings(source: Mapping[str, Any]) -> tuple[bool, bool]:
|
|
82
|
+
"""``(allow_hs, lists_hs)`` read from one source of settings."""
|
|
83
|
+
allow = str(source.get("AUTH_JWT_ALLOW_HS") or "").strip().lower() in _TRUE
|
|
84
|
+
return allow, _lists_hmac(str(source.get("AUTH_JWT_ALGORITHMS") or ""))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def jwt_hs_in_values(settings: DeploySettings, values: dict[str, Any]) -> bool:
|
|
88
|
+
"""The chart values run the ``jwt`` policy with an HS* algorithm (the pod needs the secret)."""
|
|
89
|
+
env = values.get("env") if isinstance(values.get("env"), dict) else {}
|
|
90
|
+
return _auth_policy(settings, values) == "jwt" and _hs_settings(env)[1]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def jwt_hs_indicated(
|
|
94
|
+
settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str] | None = None
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""HS* JWTs are opted into by the chart values or the env file (``jwt`` policy only)."""
|
|
97
|
+
if _auth_policy(settings, values) != "jwt":
|
|
98
|
+
return False
|
|
99
|
+
env = values.get("env") if isinstance(values.get("env"), dict) else {}
|
|
100
|
+
return any(any(_hs_settings(source)) for source in (env, file_values or {}))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def for_environment(
|
|
104
|
+
settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str] | None = None
|
|
105
|
+
) -> DeploySettings:
|
|
106
|
+
"""``settings`` whose allow-list also holds ``AUTH_JWT_SECRET`` when HS* JWTs are in use."""
|
|
107
|
+
if JWT_SECRET_KEY in settings.secret_keys or not jwt_hs_indicated(
|
|
108
|
+
settings, values, file_values
|
|
109
|
+
):
|
|
110
|
+
return settings
|
|
111
|
+
return dataclasses.replace(settings, secret_keys=[*settings.secret_keys, JWT_SECRET_KEY])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _enabled(values: dict[str, Any], section: str) -> bool:
|
|
115
|
+
block = values.get(section) if isinstance(values.get(section), dict) else {}
|
|
116
|
+
value = block.get("enabled")
|
|
117
|
+
return value is True or (
|
|
118
|
+
isinstance(value, str) and value.strip().lower() in ("true", "yes", "1")
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def required_keys(settings: DeploySettings, values: dict[str, Any]) -> list[str]:
|
|
123
|
+
"""Allow-listed keys the pods need from the Secret, in allow-list order."""
|
|
124
|
+
needed: set[str] = set()
|
|
125
|
+
provider = (_env_value(values, "MODEL_PROVIDER") or settings.model_provider).lower()
|
|
126
|
+
if provider not in KEYLESS_PROVIDERS:
|
|
127
|
+
needed.add(PROVIDER_KEY_VARS.get(provider, "MODEL_API_KEY"))
|
|
128
|
+
if _auth_policy(settings, values) == "shared-bearer":
|
|
129
|
+
needed.add("API_KEY")
|
|
130
|
+
if jwt_hs_in_values(settings, values):
|
|
131
|
+
needed.add(JWT_SECRET_KEY)
|
|
132
|
+
if settings.runtime == "langgraph-server":
|
|
133
|
+
if not _enabled(values, "postgresql"):
|
|
134
|
+
needed.add("DATABASE_URI")
|
|
135
|
+
if not _enabled(values, "redis"):
|
|
136
|
+
needed.add("REDIS_URI")
|
|
137
|
+
elif (
|
|
138
|
+
not _enabled(values, "postgresql")
|
|
139
|
+
and (_env_value(values, "CHECKPOINTER") or "postgres").lower() != "memory"
|
|
140
|
+
):
|
|
141
|
+
needed.add("POSTGRES_DSN")
|
|
142
|
+
allowed = for_environment(settings, values).secret_keys
|
|
143
|
+
return [k for k in allowed if k in needed and not _env_value(values, k)]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def optional_keys(settings: DeploySettings, values: dict[str, Any]) -> list[str]:
|
|
147
|
+
"""The allow-listed keys that are not required, in allow-list order."""
|
|
148
|
+
required = set(required_keys(settings, values))
|
|
149
|
+
return [k for k in for_environment(settings, values).secret_keys if k not in required]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def unreached_hs_settings(
|
|
153
|
+
settings: DeploySettings, values: dict[str, Any], file_values: Mapping[str, str]
|
|
154
|
+
) -> list[str]:
|
|
155
|
+
"""HS* settings the env file sets that never reach the pods.
|
|
156
|
+
|
|
157
|
+
Only allow-listed keys go from the env file into the Secret, and these are
|
|
158
|
+
plain settings that belong in the chart values' ``env`` (the ConfigMap).
|
|
159
|
+
"""
|
|
160
|
+
if _auth_policy(settings, values) != "jwt":
|
|
161
|
+
return []
|
|
162
|
+
return [
|
|
163
|
+
key
|
|
164
|
+
for key in JWT_HS_SETTINGS
|
|
165
|
+
if str(file_values.get(key) or "").strip()
|
|
166
|
+
and key not in settings.secret_keys
|
|
167
|
+
and not _env_value(values, key)
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def print_unreached_hs_settings(
|
|
172
|
+
settings: DeploySettings,
|
|
173
|
+
env: str,
|
|
174
|
+
values: dict[str, Any],
|
|
175
|
+
file_values: Mapping[str, str],
|
|
176
|
+
*,
|
|
177
|
+
source: object,
|
|
178
|
+
console: Console,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Warn that HS* settings in the env file stay local (see :func:`unreached_hs_settings`)."""
|
|
181
|
+
keys = unreached_hs_settings(settings, values, file_values)
|
|
182
|
+
if keys:
|
|
183
|
+
console.print(
|
|
184
|
+
f" {', '.join(keys)} in {source} never reach the pods (only allow-listed keys go "
|
|
185
|
+
f"into the Secret); set them under env: in {settings.values_file(env)}.",
|
|
186
|
+
style="yellow",
|
|
187
|
+
markup=False,
|
|
188
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
"""graph-agents-cli secrets commands — provision and inspect the app Secret."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from graph_agents_cli._click import LazyGroup
|
|
21
|
+
from graph_agents_cli._output import Console
|
|
22
|
+
from graph_agents_cli.deploy import _modes, _preflight
|
|
23
|
+
from graph_agents_cli.deploy._config import DeploySettings, load_settings
|
|
24
|
+
from graph_agents_cli.deploy._kube import Target
|
|
25
|
+
from graph_agents_cli.deploy._values import load_chart_values
|
|
26
|
+
from graph_agents_cli.secrets import _apply, _required
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group("secrets", cls=LazyGroup)
|
|
30
|
+
def secrets_group() -> None:
|
|
31
|
+
"""Provision and inspect the application Secret per environment.
|
|
32
|
+
|
|
33
|
+
\b
|
|
34
|
+
Subcommands:
|
|
35
|
+
apply Create or update <name>-app from the allow-listed keys of an env file
|
|
36
|
+
status List which allow-listed keys are present (values are never printed)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _resolve(
|
|
41
|
+
settings: DeploySettings, env: str, context: str | None
|
|
42
|
+
) -> tuple[_modes.ResolvedContext, Target]:
|
|
43
|
+
namespace = settings.target(env).namespace
|
|
44
|
+
resolved = _modes.resolve(settings, env, context)
|
|
45
|
+
return resolved, Target(context=resolved.name, namespace=namespace)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_CONTEXT_HELP = "Kube context to use instead of environments.<env>.context."
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _default_env_file_values(env: str) -> dict[str, str]:
|
|
52
|
+
"""The env file ``secrets apply --env <env>`` reads by default (empty when absent or unreadable)."""
|
|
53
|
+
path = _apply.resolve_env_file(env, None)
|
|
54
|
+
if path is None:
|
|
55
|
+
return {}
|
|
56
|
+
try:
|
|
57
|
+
return _apply.read_env_file(path)
|
|
58
|
+
except (OSError, UnicodeDecodeError):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@secrets_group.command("apply")
|
|
63
|
+
@click.option("--env", "env", required=True, help="Target environment (dev, staging, prod).")
|
|
64
|
+
@click.option(
|
|
65
|
+
"--env-file",
|
|
66
|
+
"env_file",
|
|
67
|
+
default=None,
|
|
68
|
+
help="Env file; defaults to .env.<env> (dev also falls back to .env).",
|
|
69
|
+
)
|
|
70
|
+
@click.option("--context", "context", default=None, help=_CONTEXT_HELP)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--yes",
|
|
73
|
+
"-y",
|
|
74
|
+
"yes",
|
|
75
|
+
is_flag=True,
|
|
76
|
+
help="Accept the kubeconfig's current context outside dev without prompting.",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--rotate-api-key",
|
|
80
|
+
"rotate_api_key",
|
|
81
|
+
is_flag=True,
|
|
82
|
+
help="Replace the live API_KEY with the one in the env file (otherwise the live key wins).",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--dry-run", "dry_run", is_flag=True, help="Print the kubectl pipeline and a redacted manifest."
|
|
86
|
+
)
|
|
87
|
+
def cmd_secrets_apply(
|
|
88
|
+
env: str,
|
|
89
|
+
env_file: str | None,
|
|
90
|
+
context: str | None,
|
|
91
|
+
yes: bool,
|
|
92
|
+
rotate_api_key: bool,
|
|
93
|
+
dry_run: bool,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Create or update the app Secret from the allow-listed keys of an env file."""
|
|
96
|
+
console = Console()
|
|
97
|
+
settings = load_settings()
|
|
98
|
+
resolved, target = _resolve(settings, env, context)
|
|
99
|
+
path = _apply.resolve_env_file(env, env_file)
|
|
100
|
+
chart_values = load_chart_values(settings.chart_dir, env)
|
|
101
|
+
if path is None:
|
|
102
|
+
raise _apply.missing_env_file_error(
|
|
103
|
+
env, _required.for_environment(settings, chart_values).secret_keys
|
|
104
|
+
)
|
|
105
|
+
values = _apply.read_env_file(path)
|
|
106
|
+
# The allow-list for this environment (AUTH_JWT_SECRET joins it for HS* JWTs).
|
|
107
|
+
settings = _required.for_environment(settings, chart_values, values)
|
|
108
|
+
_apply.check_file_values(
|
|
109
|
+
values, settings.secret_keys, rotate_api_key=rotate_api_key, source=path
|
|
110
|
+
)
|
|
111
|
+
console.print(f"Secret {settings.secret_name} in {target.namespace} from {path}")
|
|
112
|
+
_modes.announce(env, resolved, console=console)
|
|
113
|
+
_modes.confirm(
|
|
114
|
+
env, resolved, yes=yes, dry_run=dry_run, console=console, action="apply the Secret for"
|
|
115
|
+
)
|
|
116
|
+
_required.print_unreached_hs_settings(
|
|
117
|
+
settings, env, chart_values, values, source=path, console=console
|
|
118
|
+
)
|
|
119
|
+
plan = _apply.provision(
|
|
120
|
+
name=settings.secret_name,
|
|
121
|
+
env=env,
|
|
122
|
+
target=target,
|
|
123
|
+
allowed=settings.secret_keys,
|
|
124
|
+
path=path,
|
|
125
|
+
values=values,
|
|
126
|
+
rotate_api_key=rotate_api_key,
|
|
127
|
+
dry_run=dry_run,
|
|
128
|
+
console=console,
|
|
129
|
+
# Only the shared-bearer policy reads API_KEY: never mint one it would not use.
|
|
130
|
+
mint_api_key=_preflight.effective_auth_policy(settings, chart_values) == "shared-bearer",
|
|
131
|
+
metrics_name=settings.metrics_secret_name,
|
|
132
|
+
)
|
|
133
|
+
if not _modes.is_dev_env(env):
|
|
134
|
+
for key in _preflight.dsn_keys(settings, chart_values):
|
|
135
|
+
dsn = plan.data.get(key) or ""
|
|
136
|
+
if (
|
|
137
|
+
dsn
|
|
138
|
+
and dsn != _apply.PENDING_PLACEHOLDER
|
|
139
|
+
and _preflight.dsn_without_tls(chart_values, dsn)
|
|
140
|
+
):
|
|
141
|
+
console.print(
|
|
142
|
+
f" Warning: {_preflight.dsn_tls_warning(key)}", style="yellow", markup=False
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@secrets_group.command("status")
|
|
147
|
+
@click.option("--env", "env", required=True, help="Target environment (dev, staging, prod).")
|
|
148
|
+
@click.option("--context", "context", default=None, help=_CONTEXT_HELP)
|
|
149
|
+
@click.option(
|
|
150
|
+
"--strict",
|
|
151
|
+
"strict",
|
|
152
|
+
is_flag=True,
|
|
153
|
+
help="Also exit 1 when an optional allow-listed key is missing.",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--dry-run", "dry_run", is_flag=True, help="Print the kubectl command without running it."
|
|
157
|
+
)
|
|
158
|
+
def cmd_secrets_status(env: str, context: str | None, strict: bool, dry_run: bool) -> None:
|
|
159
|
+
"""List which allow-listed keys are present in the app Secret (never values).
|
|
160
|
+
|
|
161
|
+
\b
|
|
162
|
+
Exit codes (usable as a CI or pre-deploy gate):
|
|
163
|
+
0 the Secret holds every required key (optional ones may be missing)
|
|
164
|
+
1 the Secret is missing, or a required key is (any key with --strict)
|
|
165
|
+
2 kubectl failed (unreachable cluster, credentials, RBAC)
|
|
166
|
+
3 configuration error (unknown environment or kube context, no manifest)
|
|
167
|
+
Required keys follow the environment's chart values: the model provider's key
|
|
168
|
+
(not for openai-compatible), API_KEY under shared-bearer, AUTH_JWT_SECRET under
|
|
169
|
+
jwt with an HS* algorithm, and POSTGRES_DSN or DATABASE_URI/REDIS_URI unless the
|
|
170
|
+
bundled subchart provides them.
|
|
171
|
+
"""
|
|
172
|
+
console = Console()
|
|
173
|
+
settings = load_settings()
|
|
174
|
+
resolved, target = _resolve(settings, env, context)
|
|
175
|
+
_modes.announce(env, resolved, console=console)
|
|
176
|
+
_modes.require_known_context(env, resolved, dry_run=dry_run, console=console)
|
|
177
|
+
values = load_chart_values(settings.chart_dir, env)
|
|
178
|
+
# The env file `secrets apply` would read (never printed) tells whether the
|
|
179
|
+
# environment uses HS* JWTs, which adds AUTH_JWT_SECRET to the allow-list.
|
|
180
|
+
settings = _required.for_environment(settings, values, _default_env_file_values(env))
|
|
181
|
+
required = _required.required_keys(settings, values)
|
|
182
|
+
optional = [k for k in settings.secret_keys if k not in required]
|
|
183
|
+
present = _apply.secret_keys_present(
|
|
184
|
+
settings.secret_name, target, dry_run=dry_run, console=console
|
|
185
|
+
)
|
|
186
|
+
if dry_run:
|
|
187
|
+
return
|
|
188
|
+
if present is None:
|
|
189
|
+
console.print(
|
|
190
|
+
f"Secret {settings.secret_name} not found in namespace {target.namespace}.", style="red"
|
|
191
|
+
)
|
|
192
|
+
console.print(" Missing: " + ", ".join(settings.secret_keys))
|
|
193
|
+
raise SystemExit(1)
|
|
194
|
+
missing_required = [k for k in required if k not in present]
|
|
195
|
+
missing_optional = [k for k in optional if k not in present]
|
|
196
|
+
found = [k for k in settings.secret_keys if k in present]
|
|
197
|
+
extra = sorted(present - set(settings.secret_keys))
|
|
198
|
+
console.print(f"Secret {settings.secret_name} in {target.namespace}:")
|
|
199
|
+
console.print(" present: " + (", ".join(found) or "(none)"), style="green")
|
|
200
|
+
console.print(
|
|
201
|
+
" missing required: " + (", ".join(missing_required) or "(none)"),
|
|
202
|
+
style="red" if missing_required else "green",
|
|
203
|
+
)
|
|
204
|
+
console.print(
|
|
205
|
+
" missing optional: " + (", ".join(missing_optional) or "(none)"),
|
|
206
|
+
style="yellow" if missing_optional else "green",
|
|
207
|
+
)
|
|
208
|
+
if extra:
|
|
209
|
+
console.print(" not in the allow-list: " + ", ".join(extra), style="dim")
|
|
210
|
+
if missing_required or (strict and missing_optional):
|
|
211
|
+
raise SystemExit(1)
|
|
@@ -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,221 @@
|
|
|
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
|
+
"""Mirror globally-installed skills into Antigravity's skill directories.
|
|
17
|
+
|
|
18
|
+
Temporary workaround until `npx skills` fully supports Antigravity's IDE / CLI /
|
|
19
|
+
2.0 skill paths. Today `npx skills ... -g` installs global skills into the
|
|
20
|
+
canonical ``~/.agents/skills`` store, but Antigravity does not read from there:
|
|
21
|
+
the IDE and Antigravity 2.0 read ``~/.gemini/config/skills`` and the Antigravity
|
|
22
|
+
CLI reads ``~/.gemini/antigravity-cli/skills``. This module bridges that gap by
|
|
23
|
+
linking each canonical skill into both locations so a global ``graph-agents-cli setup``
|
|
24
|
+
makes the skills visible to Antigravity. Once upstream installs to the correct
|
|
25
|
+
locations directly, this module can be removed.
|
|
26
|
+
|
|
27
|
+
Both locations are mirrored ourselves rather than relying on ``npx skills``,
|
|
28
|
+
whose global install is currently unreliable (vercel-labs/skills#1362).
|
|
29
|
+
|
|
30
|
+
Links are directory symlinks, falling back to directory junctions on Windows
|
|
31
|
+
(which work without elevation). Links auto-track the canonical store, so updates
|
|
32
|
+
to a skill are seen by Antigravity with no re-link needed.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# Canonical store that ``npx skills ... -g`` writes to.
|
|
42
|
+
_CANONICAL_SUBPATH = (".agents", "skills")
|
|
43
|
+
|
|
44
|
+
# Where Antigravity looks for global skills: IDE / 2.0 first, then the CLI.
|
|
45
|
+
_ANTIGRAVITY_SUBPATHS = (
|
|
46
|
+
(".gemini", "config", "skills"),
|
|
47
|
+
(".gemini", "antigravity-cli", "skills"),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _display(path: Path, home: Path) -> str:
|
|
52
|
+
"""Render ``path`` with the home directory collapsed to ``~`` for output."""
|
|
53
|
+
try:
|
|
54
|
+
return f"~/{path.relative_to(home)}"
|
|
55
|
+
except ValueError:
|
|
56
|
+
return str(path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_link(path: Path) -> bool:
|
|
60
|
+
"""Return True if ``path`` is a symlink or a Windows directory junction.
|
|
61
|
+
|
|
62
|
+
``Path.is_symlink()`` does not report junctions, so fall back to
|
|
63
|
+
``os.readlink``, which succeeds for both and raises for real directories.
|
|
64
|
+
"""
|
|
65
|
+
if path.is_symlink():
|
|
66
|
+
return True
|
|
67
|
+
try:
|
|
68
|
+
os.readlink(path)
|
|
69
|
+
return True
|
|
70
|
+
except OSError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _link_target_into(entry: Path, source_root: Path) -> bool:
|
|
75
|
+
"""Return True if the link at ``entry`` points somewhere under ``source_root``."""
|
|
76
|
+
try:
|
|
77
|
+
target_str = os.readlink(entry)
|
|
78
|
+
# Windows-specific path prefix handling:
|
|
79
|
+
# - "\??\" is the NT Object Manager namespace prefix, often returned by
|
|
80
|
+
# os.readlink for Windows junctions.
|
|
81
|
+
# - "\\?\" is the Win32 device namespace prefix, used for long paths.
|
|
82
|
+
# Python's pathlib.Path does not recognize these prefixes as absolute roots
|
|
83
|
+
# (e.g., Path("\??\C:\foo").is_absolute() may return False or behave incorrectly),
|
|
84
|
+
# so we strip the 4-character prefix to get a standard absolute path.
|
|
85
|
+
if target_str.startswith("\\??\\"):
|
|
86
|
+
target_str = target_str[4:]
|
|
87
|
+
elif target_str.startswith("\\\\?\\"):
|
|
88
|
+
target_str = target_str[4:]
|
|
89
|
+
target = Path(target_str)
|
|
90
|
+
except OSError:
|
|
91
|
+
return False
|
|
92
|
+
if not target.is_absolute():
|
|
93
|
+
target = entry.parent / target
|
|
94
|
+
try:
|
|
95
|
+
resolved_parent = target.parent.resolve()
|
|
96
|
+
resolved_source = source_root.resolve()
|
|
97
|
+
return resolved_source == resolved_parent or resolved_source in resolved_parent.parents
|
|
98
|
+
except OSError:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _link_dir(source: Path, link: Path) -> bool:
|
|
103
|
+
"""Point ``link`` at ``source`` with a directory link (junction on Windows).
|
|
104
|
+
|
|
105
|
+
Idempotent: a link already resolving to ``source`` is left as-is. A stale or
|
|
106
|
+
mis-pointing link is replaced, but a real (non-link) file or directory is left
|
|
107
|
+
untouched and reported as a failure so user data is never clobbered.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
if link.exists() and os.path.realpath(link) == os.path.realpath(source):
|
|
111
|
+
return True
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
if _is_link(link):
|
|
116
|
+
try:
|
|
117
|
+
link.unlink()
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
elif link.exists():
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
os.symlink(source, link, target_is_directory=True)
|
|
127
|
+
return True
|
|
128
|
+
except (OSError, NotImplementedError):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# Windows without symlink privilege: a junction works without elevation.
|
|
132
|
+
if os.name == "nt":
|
|
133
|
+
import subprocess
|
|
134
|
+
|
|
135
|
+
from graph_agents_cli._runner import run_resolved
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
run_resolved(
|
|
139
|
+
["cmd", "/c", "mklink", "/J", str(link), str(source)],
|
|
140
|
+
resolve_executable=False,
|
|
141
|
+
check=True,
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
)
|
|
145
|
+
return True
|
|
146
|
+
except (OSError, subprocess.CalledProcessError):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
logging.warning(
|
|
150
|
+
"Could not link skill '%s' into %s "
|
|
151
|
+
"(on Windows, enable Developer Mode or run as administrator).",
|
|
152
|
+
source.name,
|
|
153
|
+
link.parent,
|
|
154
|
+
)
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _prune_dead_links(target_root: Path, source_root: Path) -> None:
|
|
159
|
+
"""Remove broken links under ``target_root`` that point into ``source_root``.
|
|
160
|
+
|
|
161
|
+
Keeps the mirror in sync when a skill is removed from the canonical store.
|
|
162
|
+
Only ever removes links (symlinks/junctions), never real files or directories.
|
|
163
|
+
"""
|
|
164
|
+
if not target_root.is_dir():
|
|
165
|
+
return
|
|
166
|
+
for entry in target_root.iterdir():
|
|
167
|
+
if entry.exists() or not _link_target_into(entry, source_root):
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
entry.unlink()
|
|
171
|
+
except OSError as e:
|
|
172
|
+
logging.warning("Could not remove stale skill link %s: %s", entry, e)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def link_skills_for_antigravity(
|
|
176
|
+
*, home: Path | None = None, skills_dir: Path | None = None
|
|
177
|
+
) -> list[str]:
|
|
178
|
+
"""Link each canonical global skill into Antigravity's skill directories.
|
|
179
|
+
|
|
180
|
+
No-op (returns an empty list) when Antigravity/Gemini is not present
|
|
181
|
+
(no ``~/.gemini``) or when the canonical store is missing. Best-effort:
|
|
182
|
+
individual link failures are logged, never raised.
|
|
183
|
+
|
|
184
|
+
Returns a list of human-readable summary lines describing what was linked.
|
|
185
|
+
"""
|
|
186
|
+
home = home or Path.home()
|
|
187
|
+
|
|
188
|
+
if not (home / ".gemini").is_dir():
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
source_root = skills_dir or home.joinpath(*_CANONICAL_SUBPATH)
|
|
192
|
+
if not source_root.is_dir():
|
|
193
|
+
# This runs right after npx installs skills, so a missing canonical store
|
|
194
|
+
# means npx no longer writes there — surface it instead of silently
|
|
195
|
+
# no-op'ing (see module docstring).
|
|
196
|
+
logging.warning(
|
|
197
|
+
"Skills store %s not found; skills were not mirrored to Antigravity.",
|
|
198
|
+
source_root,
|
|
199
|
+
)
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
skill_dirs = [
|
|
203
|
+
d for d in sorted(source_root.iterdir()) if d.is_dir() and (d / "SKILL.md").is_file()
|
|
204
|
+
]
|
|
205
|
+
if not skill_dirs:
|
|
206
|
+
# Same expectation as above: npx just ran, so an empty store is surprising.
|
|
207
|
+
# Fall through so dead links from previously-linked skills still get pruned.
|
|
208
|
+
logging.warning("Skills store %s has no skills to mirror to Antigravity.", source_root)
|
|
209
|
+
|
|
210
|
+
summary: list[str] = []
|
|
211
|
+
for subpath in _ANTIGRAVITY_SUBPATHS:
|
|
212
|
+
target_root = home.joinpath(*subpath)
|
|
213
|
+
# Link each skill individually rather than pointing the whole target dir
|
|
214
|
+
# at the canonical store: ~/.gemini/.../skills is owned by Antigravity and
|
|
215
|
+
# may already hold skills from other sources. A top-level symlink would
|
|
216
|
+
# fail (dir exists) or shadow those; per-skill links coexist with them.
|
|
217
|
+
linked = sum(_link_dir(skill_dir, target_root / skill_dir.name) for skill_dir in skill_dirs)
|
|
218
|
+
_prune_dead_links(target_root, source_root)
|
|
219
|
+
if linked:
|
|
220
|
+
summary.append(f"Linked {linked} skill(s) into {_display(target_root, home)}")
|
|
221
|
+
return summary
|