datex-studio-cli 0.5.1__tar.gz → 0.5.3__tar.gz
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.
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/ci.yml +29 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/PKG-INFO +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/pyproject.toml +3 -3
- datex_studio_cli-0.5.3/scripts/check_wheel_completeness.py +58 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/auth.py +28 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/onboarding.py +126 -2
- datex_studio_cli-0.5.3/src/dxs/core/quartermaster/__init__.py +5 -0
- datex_studio_cli-0.5.3/src/dxs/core/quartermaster/client.py +190 -0
- datex_studio_cli-0.5.3/src/dxs/core/quartermaster/tenants.py +57 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/__init__.py +1 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/frontmatter.py +30 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/installer.py +148 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/layout.py +57 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/lock.py +98 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/resolver.py +48 -0
- datex_studio_cli-0.5.3/src/dxs/core/skills/staleness.py +55 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/404/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/404.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/capture/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/capture/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/design/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/index.html +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/index.txt +1 -1
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.dockerignore +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/actionlint.yaml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/build-image.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/promote-prod.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.github/workflows/release.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/.gitignore +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/CONTEXT.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/README.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/.env.example +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/Dockerfile +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/README.md +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/apply-local-cache.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/collector-config.yaml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/docker-compose.yml +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/local-test-settings.json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/otel/remote-settings.json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/report-creator.skill +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/datadog_errors.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/scripts/wavelength_query.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/branch_commit_guard.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/cascade.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm_delivery.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/crm_time.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/init.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/_common.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/create.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/data.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/pack.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/ng/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/reference.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/reset.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/skills.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/commands/validation_report.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/agent_browser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/reference_graph.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/api/replacements.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/datex_tenant.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/msal_client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/tenants.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/flow_shape.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/html.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/pagination.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/resolvers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/dynamics/time_entry.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/manifest.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/mocks.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/scaffold.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/ng/serve_lock.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/py.typed +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/clipboard.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/utils/versioning.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.3/src/dxs/web/static/_next/static/Xsbcf46McNo4ycd40X4z0}/_buildManifest.js +0 -0
- {datex_studio_cli-0.5.1/src/dxs/web/static/_next/static/W6-87g3VtSLhTM2G0U3r1 → datex_studio_cli-0.5.3/src/dxs/web/static/_next/static/Xsbcf46McNo4ycd40X4z0}/_ssgManifest.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/538-b06224e65f71c9a1.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-e3c660196c244183.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-c083dc9f01f32a04.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/design/page-689cd5b9e1d9a74e.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/layout-cec86f90e385371d.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/app/page-f0380b0f29983264.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/main-app-694cf3568b6c9fa9.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.5.1 → datex_studio_cli-0.5.3}/src/dxs/web/static/report-validate.html +0 -0
|
@@ -59,3 +59,32 @@ jobs:
|
|
|
59
59
|
- run: uv run ruff check quartermaster
|
|
60
60
|
- run: uv run --package quartermaster mypy quartermaster/src/quartermaster
|
|
61
61
|
- run: uv run --package quartermaster pytest quartermaster/tests -q
|
|
62
|
+
|
|
63
|
+
# The artifact nobody was looking at. 0.5.0 and 0.5.1 both shipped without
|
|
64
|
+
# dxs.core.quartermaster and dxs.core.skills, and 0.5.1 could not run
|
|
65
|
+
# `dxs --version` at all -- while this suite was fully green, because tests
|
|
66
|
+
# import from the source tree and never open the wheel. release.yml calls
|
|
67
|
+
# this workflow, so a broken artifact now blocks the release too.
|
|
68
|
+
package:
|
|
69
|
+
runs-on: arc-dxs
|
|
70
|
+
timeout-minutes: 15
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v7
|
|
73
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
74
|
+
with:
|
|
75
|
+
enable-cache: true
|
|
76
|
+
cache-suffix: package
|
|
77
|
+
download-from-astral-mirror: false
|
|
78
|
+
- run: uv python install 3.13
|
|
79
|
+
- run: uv build
|
|
80
|
+
- name: Every package under src/ must be in the wheel
|
|
81
|
+
run: uv run python scripts/check_wheel_completeness.py
|
|
82
|
+
- name: The built wheel must actually run
|
|
83
|
+
run: |
|
|
84
|
+
# A clean venv, not the workspace: importing from the source tree is
|
|
85
|
+
# exactly what hid this. Resolve deps from the index like a user does.
|
|
86
|
+
uv venv /tmp/smoke
|
|
87
|
+
uv pip install --python /tmp/smoke/bin/python dist/*.whl
|
|
88
|
+
/tmp/smoke/bin/dxs --version
|
|
89
|
+
/tmp/smoke/bin/dxs skills --help > /dev/null
|
|
90
|
+
/tmp/smoke/bin/python -c "import dxs.core.quartermaster, dxs.core.skills"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datex-studio-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: CLI for Datex Studio low-code platform, designed for LLM-based AI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/datex/datex-studio-cli
|
|
6
6
|
Project-URL: Documentation, https://github.com/datex/datex-studio-cli
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datex-studio-cli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3"
|
|
8
8
|
description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -94,7 +94,7 @@ exclude = [
|
|
|
94
94
|
"documentation/",
|
|
95
95
|
"exploration/",
|
|
96
96
|
"exclude/",
|
|
97
|
-
"skills/",
|
|
97
|
+
"/skills/",
|
|
98
98
|
"tests/",
|
|
99
99
|
"publish.sh",
|
|
100
100
|
"*.analysis.md",
|
|
@@ -102,7 +102,7 @@ exclude = [
|
|
|
102
102
|
"uv.lock",
|
|
103
103
|
"src/dxs/web/frontend/",
|
|
104
104
|
"example-reports/",
|
|
105
|
-
"quartermaster/",
|
|
105
|
+
"/quartermaster/",
|
|
106
106
|
]
|
|
107
107
|
|
|
108
108
|
[tool.uv.workspace]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Assert the built wheel carries every package that exists under src/dxs.
|
|
3
|
+
|
|
4
|
+
Versions 0.5.0 and 0.5.1 both shipped without ``dxs.core.quartermaster`` and
|
|
5
|
+
``dxs.core.skills``. The sdist exclude list named ``skills/`` and
|
|
6
|
+
``quartermaster/`` to drop the top-level directories of those names; hatchling
|
|
7
|
+
matches an unanchored pattern at any depth, so it dropped the nested packages
|
|
8
|
+
too, and ``uv build`` builds the wheel from the sdist it just made. 0.5.1 could
|
|
9
|
+
not even run ``dxs --version``.
|
|
10
|
+
|
|
11
|
+
A full green suite said nothing about any of it: tests import from the source
|
|
12
|
+
tree and never open the artifact. This looks at the artifact.
|
|
13
|
+
|
|
14
|
+
Usage: check_wheel_completeness.py [dist_dir]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import zipfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
+
SRC = ROOT / "src"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
dist = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "dist"
|
|
29
|
+
wheels = sorted(dist.glob("*.whl"))
|
|
30
|
+
if not wheels:
|
|
31
|
+
print(f"no wheel in {dist} — run `uv build` first", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
wheel = wheels[-1]
|
|
34
|
+
|
|
35
|
+
# Every directory holding an __init__.py is a package that must ship.
|
|
36
|
+
expected = {
|
|
37
|
+
p.parent.relative_to(SRC).as_posix() for p in SRC.rglob("__init__.py")
|
|
38
|
+
if "__pycache__" not in p.parts
|
|
39
|
+
}
|
|
40
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
41
|
+
shipped = {n.rsplit("/", 1)[0] for n in names if n.endswith("/__init__.py")}
|
|
42
|
+
|
|
43
|
+
missing = sorted(expected - shipped)
|
|
44
|
+
if missing:
|
|
45
|
+
print(f"{wheel.name} is missing {len(missing)} package(s) present in src/:",
|
|
46
|
+
file=sys.stderr)
|
|
47
|
+
for m in missing:
|
|
48
|
+
print(f" {m}", file=sys.stderr)
|
|
49
|
+
print("\nUsually an unanchored exclude pattern in pyproject.toml matching a "
|
|
50
|
+
"nested path — anchor it with a leading slash.", file=sys.stderr)
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
print(f"{wheel.name}: all {len(expected)} packages present")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
raise SystemExit(main())
|
|
@@ -32,14 +32,38 @@ from dxs.core.auth.token_cache import CachedIdentity, get_identity_override
|
|
|
32
32
|
from dxs.core.footprint import get_footprint_token
|
|
33
33
|
from dxs.core.onboarding import (
|
|
34
34
|
LoginPresenter,
|
|
35
|
+
apply_sign_in_tenant_hint,
|
|
35
36
|
human_present,
|
|
36
37
|
map_login_failure,
|
|
38
|
+
missing_service_principal_tenant,
|
|
37
39
|
)
|
|
40
|
+
from dxs.core.quartermaster.tenants import lookup_sign_in_tenant
|
|
38
41
|
from dxs.utils.errors import AuthenticationError, DxsError, ValidationError
|
|
39
42
|
from dxs.utils.responses import list_response, single
|
|
40
43
|
from dxs.utils.restricted import restrict_in_restricted_mode
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
def _sharpen_missing_service_principal(
|
|
47
|
+
mapped: AuthenticationError, tenant_id: str | None, raw_error: str
|
|
48
|
+
) -> AuthenticationError:
|
|
49
|
+
"""Ask Quartermaster which tenant the org actually signs in against.
|
|
50
|
+
|
|
51
|
+
Only for DXS-AUTH-030, only after the login already failed, and only ever
|
|
52
|
+
additive: any problem reaching the service leaves the mapped notice as-is.
|
|
53
|
+
The tenant comes from the caller when it resolved one, and otherwise out of
|
|
54
|
+
Entra's own error text — a blank-prompt or org-name login never had one.
|
|
55
|
+
"""
|
|
56
|
+
requested = tenant_id or missing_service_principal_tenant(raw_error)
|
|
57
|
+
if not requested:
|
|
58
|
+
return mapped
|
|
59
|
+
try:
|
|
60
|
+
return apply_sign_in_tenant_hint(
|
|
61
|
+
mapped, requested_tenant=requested, sign_in=lookup_sign_in_tenant(requested)
|
|
62
|
+
)
|
|
63
|
+
except Exception: # noqa: BLE001 - never worsen a diagnosed failure
|
|
64
|
+
return mapped
|
|
65
|
+
|
|
66
|
+
|
|
43
67
|
def _reject_tenant_override(ctx: DxsContext, command: str) -> None:
|
|
44
68
|
"""Block --tenant on identity-management commands.
|
|
45
69
|
|
|
@@ -333,6 +357,10 @@ def run_standard_login(
|
|
|
333
357
|
mapped = map_login_failure(
|
|
334
358
|
e, external=tenant_scoped, tenant_id=tenant_id, domain=domain, upn=upn
|
|
335
359
|
)
|
|
360
|
+
if mapped.code == "DXS-AUTH-030":
|
|
361
|
+
mapped = _sharpen_missing_service_principal(
|
|
362
|
+
mapped, tenant_id, f"{e.message} {e.details}"
|
|
363
|
+
)
|
|
336
364
|
if mapped is e:
|
|
337
365
|
raise
|
|
338
366
|
raise mapped from e
|
|
@@ -8,6 +8,7 @@ keeps a hard rule: machine envelopes go to stdout, human chrome to stderr.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
+
import re
|
|
11
12
|
import shutil
|
|
12
13
|
import sys
|
|
13
14
|
from dataclasses import dataclass, field
|
|
@@ -19,6 +20,7 @@ import click
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from dxs.context import DxsContext
|
|
21
22
|
from dxs.core.api.client import ApiClient
|
|
23
|
+
from dxs.core.quartermaster.tenants import SignInTenant
|
|
22
24
|
from dxs.utils.errors import AuthenticationError
|
|
23
25
|
|
|
24
26
|
|
|
@@ -151,6 +153,51 @@ def detect_profile(client: "ApiClient") -> OnboardingProfile:
|
|
|
151
153
|
)
|
|
152
154
|
|
|
153
155
|
|
|
156
|
+
# AADSTS650052: the tenant holds no service principal for a Datex app, so the
|
|
157
|
+
# sign-in cannot even reach a consent screen. Its prose ("Contact your IT Admin
|
|
158
|
+
# ... or consent to the application") reads as consent-required and is not: no
|
|
159
|
+
# consent granted in the tenant named by the error helps a user whose account
|
|
160
|
+
# lives in a different one, which is the common cause — a work-email domain
|
|
161
|
+
# resolves to the org's own corporate tenant (`homeTenantId`), while Datex
|
|
162
|
+
# provisioned `wavelength<org>.onmicrosoft.com` for them and invited their staff
|
|
163
|
+
# into that. Detected and answered ahead of the consent family for that reason.
|
|
164
|
+
_MISSING_SP_MARKERS = (
|
|
165
|
+
"aadsts650052",
|
|
166
|
+
"lacks a service principal",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# The tenant is named in the error text, which is the only source on paths where
|
|
170
|
+
# the caller never resolved one (a blank prompt, an org-name login).
|
|
171
|
+
_MISSING_SP_TENANT = re.compile(
|
|
172
|
+
r"organization\s+'?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'?",
|
|
173
|
+
re.IGNORECASE,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_missing_service_principal_error(error_text: str) -> bool:
|
|
178
|
+
"""True when the tenant signed in against has no Datex service principal."""
|
|
179
|
+
text = (error_text or "").lower()
|
|
180
|
+
return any(marker in text for marker in _MISSING_SP_MARKERS)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def missing_service_principal_tenant(error_text: str) -> str | None:
|
|
184
|
+
"""The tenant ID Entra named in an AADSTS650052 failure, if it is there."""
|
|
185
|
+
match = _MISSING_SP_TENANT.search(error_text or "")
|
|
186
|
+
return match.group(1) if match else None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def render_missing_service_principal_notice(tenant_id: str, domain: str | None) -> str:
|
|
190
|
+
"""Actionable notice for a sign-in scoped to a tenant Datex does not live in."""
|
|
191
|
+
where = f" ({domain})" if domain else ""
|
|
192
|
+
tenant = tenant_id or "the tenant you signed in against"
|
|
193
|
+
return (
|
|
194
|
+
f"AADSTS650052: tenant {tenant}{where} has no service principal for the Datex apps, "
|
|
195
|
+
"so no Datex sign-in against it can succeed. A work-email domain resolves to your "
|
|
196
|
+
"organization's own corporate tenant, which is not where your Datex account lives "
|
|
197
|
+
"when Datex provisioned a wavelength<org>.onmicrosoft.com tenant for your organization."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
154
201
|
# Admin-consent-required failure markers. AADSTS65001 = user/admin has not consented;
|
|
155
202
|
# AADSTS90094 = admin consent required. VERIFY these against a real un-consented tenant
|
|
156
203
|
# during rollout — device-code flow may surface the code in either the primary result
|
|
@@ -165,8 +212,15 @@ _CONSENT_ERROR_MARKERS = (
|
|
|
165
212
|
|
|
166
213
|
|
|
167
214
|
def is_consent_required_error(error_text: str) -> bool:
|
|
168
|
-
"""True when an auth failure indicates the app is not consented in the tenant.
|
|
215
|
+
"""True when an auth failure indicates the app is not consented in the tenant.
|
|
216
|
+
|
|
217
|
+
A missing service principal (AADSTS650052) is deliberately excluded: its text
|
|
218
|
+
carries both "consent" and "IT Admin", so the heuristic below claims it, and
|
|
219
|
+
every remedy that follows from that answer points at the wrong tenant.
|
|
220
|
+
"""
|
|
169
221
|
text = (error_text or "").lower()
|
|
222
|
+
if is_missing_service_principal_error(text):
|
|
223
|
+
return False
|
|
170
224
|
if any(marker in text for marker in _CONSENT_ERROR_MARKERS):
|
|
171
225
|
return True
|
|
172
226
|
# Catch phrasings like "admin approval required" / "has not consented".
|
|
@@ -248,7 +302,9 @@ def map_login_failure(
|
|
|
248
302
|
are external-tenant only: both are meaningless for the home tenant, where
|
|
249
303
|
the app is consented and every user is a member. The Conditional Access
|
|
250
304
|
notice is not, because that policy is configured per tenant and the home
|
|
251
|
-
tenant can adopt it like any other.
|
|
305
|
+
tenant can adopt it like any other. Neither is the missing-service-principal
|
|
306
|
+
notice: it names the tenant Entra refused, which it carries in its own text,
|
|
307
|
+
so it stays diagnostic on the paths where the caller resolved no tenant at all.
|
|
252
308
|
"""
|
|
253
309
|
from dxs.utils.errors import AuthenticationError
|
|
254
310
|
|
|
@@ -266,6 +322,22 @@ def map_login_failure(
|
|
|
266
322
|
"If you are on a remote host, sign in locally and copy ~/.datex/credentials.yaml",
|
|
267
323
|
],
|
|
268
324
|
)
|
|
325
|
+
if is_missing_service_principal_error(combined):
|
|
326
|
+
tenant = tenant_id or missing_service_principal_tenant(combined) or ""
|
|
327
|
+
consent_hint = "dxs auth consent" + (f" --tenant-id {tenant}" if tenant else "")
|
|
328
|
+
return AuthenticationError(
|
|
329
|
+
message=render_missing_service_principal_notice(tenant, domain),
|
|
330
|
+
code="DXS-AUTH-030",
|
|
331
|
+
suggestions=[
|
|
332
|
+
"dxs init --domain wavelength<org>.onmicrosoft.com "
|
|
333
|
+
"# sign in against the tenant Datex provisioned for you",
|
|
334
|
+
"Sign in there with the same work email — the tenant only decides which "
|
|
335
|
+
"directory issues the token, not which credentials you type",
|
|
336
|
+
"Ask Datex support which Entra tenant your organization signs in against",
|
|
337
|
+
"If your organization does sign in with its own tenant, a tenant admin "
|
|
338
|
+
f"must grant consent first: {consent_hint}",
|
|
339
|
+
],
|
|
340
|
+
)
|
|
269
341
|
if not external:
|
|
270
342
|
return error
|
|
271
343
|
if upn and is_guest_account_error(combined):
|
|
@@ -286,6 +358,58 @@ def map_login_failure(
|
|
|
286
358
|
return error
|
|
287
359
|
|
|
288
360
|
|
|
361
|
+
def apply_sign_in_tenant_hint(
|
|
362
|
+
error: "AuthenticationError",
|
|
363
|
+
*,
|
|
364
|
+
requested_tenant: str,
|
|
365
|
+
sign_in: "SignInTenant | None",
|
|
366
|
+
) -> "AuthenticationError":
|
|
367
|
+
"""Replace the generic AADSTS650052 advice with this org's actual tenant.
|
|
368
|
+
|
|
369
|
+
:func:`map_login_failure` can only describe the *shape* of the fix, since
|
|
370
|
+
nothing local knows which directory a given organization signs in against.
|
|
371
|
+
Given Quartermaster's answer, the advice collapses to one command — or, when
|
|
372
|
+
the tenant that refused the login is the right one after all, to the consent
|
|
373
|
+
route instead. ``sign_in`` of None (unknown org, service unreachable, older
|
|
374
|
+
service) leaves the generic notice exactly as it was.
|
|
375
|
+
"""
|
|
376
|
+
from dxs.utils.errors import AuthenticationError
|
|
377
|
+
|
|
378
|
+
if sign_in is None:
|
|
379
|
+
return error
|
|
380
|
+
|
|
381
|
+
if sign_in.tenant_id.strip().lower() == requested_tenant.strip().lower():
|
|
382
|
+
return AuthenticationError(
|
|
383
|
+
message=(
|
|
384
|
+
f"AADSTS650052: {requested_tenant} is the tenant your organization signs in "
|
|
385
|
+
"against, so the Datex apps have simply never been consented there — no "
|
|
386
|
+
"service principal for them exists in it yet."
|
|
387
|
+
),
|
|
388
|
+
code="DXS-AUTH-030",
|
|
389
|
+
suggestions=[
|
|
390
|
+
f"A tenant admin runs once: dxs auth consent --tenant-id {requested_tenant}",
|
|
391
|
+
"A blank Microsoft page carrying admin_consent=True is success, not a failure",
|
|
392
|
+
"Then retry the login",
|
|
393
|
+
],
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
target = sign_in.domain or sign_in.tenant_id
|
|
397
|
+
flag = f"--domain {sign_in.domain}" if sign_in.domain else f"--tenant-id {sign_in.tenant_id}"
|
|
398
|
+
return AuthenticationError(
|
|
399
|
+
message=(
|
|
400
|
+
f"AADSTS650052: your organization signs in against {target}, not tenant "
|
|
401
|
+
f"{requested_tenant} — that one is only where your work-email domain resolves, "
|
|
402
|
+
"and the Datex apps do not exist in it."
|
|
403
|
+
),
|
|
404
|
+
code="DXS-AUTH-030",
|
|
405
|
+
suggestions=[
|
|
406
|
+
f"dxs init {flag}",
|
|
407
|
+
"Sign in there with the same work email — you are a guest in that directory, so "
|
|
408
|
+
"your own organization's credentials still apply",
|
|
409
|
+
],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
289
413
|
def browser_allowed(no_browser: bool = False) -> bool:
|
|
290
414
|
"""Whether login chrome may open (or be) a browser.
|
|
291
415
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""The shared Quartermaster HTTP helper for interactive families (spec §16).
|
|
2
|
+
|
|
3
|
+
House rules it enforces so every family inherits them:
|
|
4
|
+
|
|
5
|
+
- ``User-Agent: dxs-cli/{__version__}`` on every request — the per-version
|
|
6
|
+
traffic view the sunset rule reads.
|
|
7
|
+
- A bare 404 on a family route means the service predates the CLI: mapped to
|
|
8
|
+
a plain "doesn't serve {family}/{segment}" error. A 404 *with* the
|
|
9
|
+
structured ``{"error": {...}}`` body is the family's own answer and is
|
|
10
|
+
surfaced as-is.
|
|
11
|
+
- Tolerant reader: unknown response fields are ignored; genuine failures
|
|
12
|
+
surface the service's machine-readable code — fail loud, never guess.
|
|
13
|
+
|
|
14
|
+
Telemetry does not use this helper (fire-and-forget, frozen sender).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from dxs import __version__
|
|
25
|
+
from dxs.core.auth.decorators import get_access_token
|
|
26
|
+
from dxs.utils.config import get_settings
|
|
27
|
+
from dxs.utils.errors import AuthenticationError, DxsError
|
|
28
|
+
|
|
29
|
+
_FAMILY_ROUTE = re.compile(r"^/(?P<family>[a-z][a-z0-9_-]*)/(?P<segment>v\d+)(?:/|$)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QuartermasterError(DxsError):
|
|
33
|
+
"""A Quartermaster request failed; ``service_code`` carries the structured code, if any."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
message: str,
|
|
38
|
+
code: str = "DXS-QM-003",
|
|
39
|
+
*,
|
|
40
|
+
service_code: str | None = None,
|
|
41
|
+
status_code: int | None = None,
|
|
42
|
+
suggestions: list[str] | None = None,
|
|
43
|
+
details: dict[str, Any] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
details = dict(details or {})
|
|
46
|
+
if service_code is not None:
|
|
47
|
+
details["service_code"] = service_code
|
|
48
|
+
if status_code is not None:
|
|
49
|
+
details["status_code"] = status_code
|
|
50
|
+
super().__init__(code=code, message=message, details=details, suggestions=suggestions)
|
|
51
|
+
self.service_code = service_code
|
|
52
|
+
self.status_code = status_code
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _family_and_segment(path: str) -> str:
|
|
56
|
+
match = _FAMILY_ROUTE.match(path)
|
|
57
|
+
if match is None:
|
|
58
|
+
return path
|
|
59
|
+
return f"{match.group('family')}/{match.group('segment')}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _structured_error(response: httpx.Response) -> dict[str, Any] | None:
|
|
63
|
+
try:
|
|
64
|
+
body = response.json()
|
|
65
|
+
except ValueError:
|
|
66
|
+
return None
|
|
67
|
+
error = body.get("error") if isinstance(body, dict) else None
|
|
68
|
+
return error if isinstance(error, dict) else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class QuartermasterClient:
|
|
72
|
+
def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None:
|
|
73
|
+
resolved = base_url if base_url is not None else get_settings().quartermaster_base_url
|
|
74
|
+
if not resolved:
|
|
75
|
+
raise QuartermasterError(
|
|
76
|
+
"Quartermaster base URL is not configured",
|
|
77
|
+
code="DXS-QM-001",
|
|
78
|
+
suggestions=[
|
|
79
|
+
"Set DXS_QUARTERMASTER_BASE_URL, or quartermaster_base_url in "
|
|
80
|
+
"~/.datex/config.yaml",
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
self.base_url = resolved.rstrip("/")
|
|
84
|
+
self._timeout = timeout
|
|
85
|
+
self._client: httpx.Client | None = None
|
|
86
|
+
|
|
87
|
+
# -- lifecycle -------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def _http(self) -> httpx.Client:
|
|
90
|
+
if self._client is None:
|
|
91
|
+
self._client = httpx.Client(
|
|
92
|
+
base_url=self.base_url,
|
|
93
|
+
timeout=self._timeout,
|
|
94
|
+
verify=get_settings().verify_ssl,
|
|
95
|
+
headers={"User-Agent": f"dxs-cli/{__version__}"},
|
|
96
|
+
)
|
|
97
|
+
return self._client
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
if self._client is not None:
|
|
101
|
+
self._client.close()
|
|
102
|
+
self._client = None
|
|
103
|
+
|
|
104
|
+
def __enter__(self) -> QuartermasterClient:
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, *_: Any) -> None:
|
|
108
|
+
self.close()
|
|
109
|
+
|
|
110
|
+
# -- requests --------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _request(
|
|
113
|
+
self, path: str, params: dict[str, Any] | None, accept: str, *, anonymous: bool = False
|
|
114
|
+
) -> httpx.Response:
|
|
115
|
+
"""``anonymous`` omits the bearer entirely — not merely optional.
|
|
116
|
+
|
|
117
|
+
The bootstrap route (`/tenants/v1/signin-tenant`) is reached while a
|
|
118
|
+
login is failing, so there is no token to attach and asking for one
|
|
119
|
+
would raise where the caller needs an answer or a shrug.
|
|
120
|
+
"""
|
|
121
|
+
headers = {"Accept": accept}
|
|
122
|
+
if not anonymous:
|
|
123
|
+
headers["Authorization"] = f"Bearer {get_access_token()}"
|
|
124
|
+
try:
|
|
125
|
+
response = self._http().get(path, params=params, headers=headers)
|
|
126
|
+
except httpx.HTTPError as exc:
|
|
127
|
+
raise QuartermasterError(
|
|
128
|
+
f"Could not reach Quartermaster at {self.base_url}: {exc}",
|
|
129
|
+
code="DXS-QM-004",
|
|
130
|
+
suggestions=[
|
|
131
|
+
"Check your network connection",
|
|
132
|
+
"If you point at a staging or local instance, verify "
|
|
133
|
+
"DXS_QUARTERMASTER_BASE_URL",
|
|
134
|
+
],
|
|
135
|
+
) from exc
|
|
136
|
+
self._raise_for_status(path, response)
|
|
137
|
+
return response
|
|
138
|
+
|
|
139
|
+
def _raise_for_status(self, path: str, response: httpx.Response) -> None:
|
|
140
|
+
if response.is_success:
|
|
141
|
+
return
|
|
142
|
+
status = response.status_code
|
|
143
|
+
if status == 401:
|
|
144
|
+
raise AuthenticationError(
|
|
145
|
+
message="Quartermaster rejected the access token",
|
|
146
|
+
code="DXS-AUTH-001",
|
|
147
|
+
suggestions=["Run 'dxs auth login' to refresh your session"],
|
|
148
|
+
)
|
|
149
|
+
error = _structured_error(response)
|
|
150
|
+
if error is not None:
|
|
151
|
+
message = str(error.get("message") or f"Quartermaster returned {status}")
|
|
152
|
+
extra = {k: v for k, v in error.items() if k not in ("code", "message")}
|
|
153
|
+
raise QuartermasterError(
|
|
154
|
+
message,
|
|
155
|
+
service_code=str(error.get("code")) if error.get("code") else None,
|
|
156
|
+
status_code=status,
|
|
157
|
+
details=extra or None,
|
|
158
|
+
)
|
|
159
|
+
if status == 404:
|
|
160
|
+
route = _family_and_segment(path)
|
|
161
|
+
raise QuartermasterError(
|
|
162
|
+
f"Quartermaster at {self.base_url} doesn't serve {route} — "
|
|
163
|
+
"the service may be older than your CLI",
|
|
164
|
+
code="DXS-QM-002",
|
|
165
|
+
status_code=status,
|
|
166
|
+
suggestions=[
|
|
167
|
+
"Check DXS_QUARTERMASTER_BASE_URL points at a current Quartermaster",
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
raise QuartermasterError(
|
|
171
|
+
f"Quartermaster returned HTTP {status} for {path}",
|
|
172
|
+
status_code=status,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def get_json(
|
|
176
|
+
self, path: str, params: dict[str, Any] | None = None, *, anonymous: bool = False
|
|
177
|
+
) -> Any:
|
|
178
|
+
response = self._request(path, params, "application/json", anonymous=anonymous)
|
|
179
|
+
try:
|
|
180
|
+
return response.json()
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
raise QuartermasterError(
|
|
183
|
+
f"Quartermaster returned a non-JSON body for {path}", status_code=200
|
|
184
|
+
) from exc
|
|
185
|
+
|
|
186
|
+
def get_bytes(
|
|
187
|
+
self, path: str, params: dict[str, Any] | None = None
|
|
188
|
+
) -> tuple[bytes, dict[str, str]]:
|
|
189
|
+
response = self._request(path, params, "application/gzip, application/json")
|
|
190
|
+
return response.content, {k.lower(): v for k, v in response.headers.items()}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Which Entra tenant an organization signs in against (`/tenants/v1/`).
|
|
2
|
+
|
|
3
|
+
The bootstrap half of the AADSTS650052 recovery: a work-email domain resolves,
|
|
4
|
+
through Entra's public discovery document, to the caller's own corporate
|
|
5
|
+
tenant — which for every org Datex provisioned a `wavelength<org>.onmicrosoft.com`
|
|
6
|
+
tenant for is the wrong directory. Quartermaster holds the org records that map
|
|
7
|
+
one to the other and answers anonymously, which is what makes it reachable from
|
|
8
|
+
a login that has just failed.
|
|
9
|
+
|
|
10
|
+
Every failure is a shrug. This runs on an error path, after a login already
|
|
11
|
+
failed with a diagnosis in hand: a Quartermaster that is down, slow, blocked by
|
|
12
|
+
a corporate proxy, older than this CLI, or simply not configured must leave that
|
|
13
|
+
diagnosis alone rather than replace it with a worse one.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from dxs.core.quartermaster.client import QuartermasterClient
|
|
21
|
+
|
|
22
|
+
# Short on purpose: a failed login is already waiting on this.
|
|
23
|
+
_TIMEOUT_SECONDS = 5.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SignInTenant:
|
|
28
|
+
"""The directory an org authenticates against; ``domain`` when one is recorded."""
|
|
29
|
+
|
|
30
|
+
tenant_id: str
|
|
31
|
+
domain: str | None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def lookup_sign_in_tenant(tenant_id: str) -> SignInTenant | None:
|
|
35
|
+
"""Resolve ``tenant_id`` to the tenant its organization signs in against.
|
|
36
|
+
|
|
37
|
+
Returns None for every failure and for an unrecognized tenant — callers use
|
|
38
|
+
it to sharpen an error, never to decide whether one occurred.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
with QuartermasterClient(timeout=_TIMEOUT_SECONDS) as client:
|
|
42
|
+
body = client.get_json(
|
|
43
|
+
"/tenants/v1/signin-tenant", {"tenant": tenant_id}, anonymous=True
|
|
44
|
+
)
|
|
45
|
+
except Exception: # noqa: BLE001 - best-effort by contract; see module docstring
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if not isinstance(body, dict):
|
|
49
|
+
return None
|
|
50
|
+
resolved = body.get("sign_in_tenant_id")
|
|
51
|
+
if not isinstance(resolved, str) or not resolved.strip():
|
|
52
|
+
return None
|
|
53
|
+
domain = body.get("sign_in_domain")
|
|
54
|
+
return SignInTenant(
|
|
55
|
+
tenant_id=resolved.strip(),
|
|
56
|
+
domain=domain.strip() if isinstance(domain, str) and domain.strip() else None,
|
|
57
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Skill management (``dxs skills``, issue #95): layout, lockfile, installer, resolver."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Read a skill's frontmatter from an installed copy (offline ``remove``/``doctor``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_frontmatter(text: str) -> dict[str, Any]:
|
|
12
|
+
if not text.startswith("---"):
|
|
13
|
+
return {}
|
|
14
|
+
end = text.find("\n---", 3)
|
|
15
|
+
if end == -1:
|
|
16
|
+
return {}
|
|
17
|
+
try:
|
|
18
|
+
doc = yaml.safe_load(text[3:end])
|
|
19
|
+
except yaml.YAMLError:
|
|
20
|
+
return {}
|
|
21
|
+
return doc if isinstance(doc, dict) else {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def installed_depends(skill_dir: Path) -> list[str]:
|
|
25
|
+
skill_md = skill_dir / "SKILL.md"
|
|
26
|
+
if not skill_md.is_file():
|
|
27
|
+
return []
|
|
28
|
+
meta = parse_frontmatter(skill_md.read_text(encoding="utf-8", errors="replace"))
|
|
29
|
+
raw = meta.get("depends") or []
|
|
30
|
+
return [str(d) for d in raw] if isinstance(raw, list) else []
|