lightnow-cli 1.6.0__tar.gz → 1.7.0__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.
Files changed (56) hide show
  1. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/.github/workflows/ci.yml +20 -1
  2. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/.github/workflows/release.yml +3 -3
  3. lightnow_cli-1.7.0/.release-please-manifest.json +3 -0
  4. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/CHANGELOG.md +16 -0
  5. lightnow_cli-1.7.0/PKG-INFO +138 -0
  6. lightnow_cli-1.7.0/README.md +90 -0
  7. lightnow_cli-1.7.0/ci/create_client_artifact.py +126 -0
  8. lightnow_cli-1.7.0/ci/test_client_artifact.py +124 -0
  9. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/docs/release.md +23 -0
  10. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/__init__.py +1 -1
  11. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/integrations.py +111 -10
  12. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/query.py +1 -1
  13. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/pyproject.toml +1 -1
  14. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/renovate.json +2 -2
  15. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_cli.py +44 -2
  16. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_integrations.py +305 -1
  17. lightnow_cli-1.6.0/.release-please-manifest.json +0 -3
  18. lightnow_cli-1.6.0/PKG-INFO +0 -344
  19. lightnow_cli-1.6.0/README.md +0 -296
  20. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/.github/workflows/release-please.yml +0 -0
  21. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/.gitignore +0 -0
  22. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/CONTRIBUTING.md +0 -0
  23. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/LICENSE +0 -0
  24. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/Makefile +0 -0
  25. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/SECURITY.md +0 -0
  26. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/docs/development.md +0 -0
  27. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/examples/README.md +0 -0
  28. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/examples/docs.md +0 -0
  29. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/examples/openapi.json +0 -0
  30. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/examples/server.json +0 -0
  31. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/authenticated_http.py +0 -0
  32. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/client.py +0 -0
  33. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/__init__.py +0 -0
  34. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/auth.py +0 -0
  35. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/context.py +0 -0
  36. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/publish.py +0 -0
  37. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/runner.py +0 -0
  38. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/updates.py +0 -0
  39. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/commands/validate.py +0 -0
  40. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/config.py +0 -0
  41. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/main.py +0 -0
  42. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/target.py +0 -0
  43. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/tls.py +0 -0
  44. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/updates.py +0 -0
  45. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/lightnow_cli/validation.py +0 -0
  46. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/release-please-config.json +0 -0
  47. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/__init__.py +0 -0
  48. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_auth_flow.py +0 -0
  49. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_authenticated_http.py +0 -0
  50. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_client.py +0 -0
  51. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_config.py +0 -0
  52. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_connection_target.py +0 -0
  53. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_context.py +0 -0
  54. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_tls.py +0 -0
  55. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_updates.py +0 -0
  56. {lightnow_cli-1.6.0 → lightnow_cli-1.7.0}/tests/test_validation.py +0 -0
@@ -73,8 +73,27 @@ jobs:
73
73
  - name: Build and check distributions
74
74
  run: make package
75
75
 
76
+ - name: Verify distribution provenance contract
77
+ run: python -m unittest discover -s ci -p 'test_*.py'
78
+
79
+ - name: Bind PR distribution provenance
80
+ if: github.event_name == 'pull_request'
81
+ env:
82
+ ARTIFACT_REPOSITORY: ${{ github.repository }}
83
+ ARTIFACT_PR_NUMBER: ${{ github.event.pull_request.number }}
84
+ ARTIFACT_HEAD_REVISION: ${{ github.event.pull_request.head.sha }}
85
+ ARTIFACT_RUN_ID: ${{ github.run_id }}
86
+ ARTIFACT_RUN_ATTEMPT: ${{ github.run_attempt }}
87
+ run: |
88
+ python ci/create_client_artifact.py --component cli \
89
+ --repository "$ARTIFACT_REPOSITORY" \
90
+ --pr-number "$ARTIFACT_PR_NUMBER" \
91
+ --head-revision "$ARTIFACT_HEAD_REVISION" \
92
+ --run-id "$ARTIFACT_RUN_ID" \
93
+ --run-attempt "$ARTIFACT_RUN_ATTEMPT"
94
+
76
95
  - name: Upload distributions
77
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
96
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
78
97
  with:
79
98
  name: python-distributions
80
99
  path: dist/*
@@ -51,7 +51,7 @@ jobs:
51
51
  run: make package
52
52
 
53
53
  - name: Upload distributions
54
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
54
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
55
55
  with:
56
56
  name: python-distributions
57
57
  path: dist/*
@@ -66,7 +66,7 @@ jobs:
66
66
 
67
67
  steps:
68
68
  - name: Download distributions
69
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
69
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
70
70
  with:
71
71
  name: python-distributions
72
72
  path: dist
@@ -89,7 +89,7 @@ jobs:
89
89
 
90
90
  steps:
91
91
  - name: Download distributions
92
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
92
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
93
93
  with:
94
94
  name: python-distributions
95
95
  path: dist
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.7.0"
3
+ }
@@ -4,6 +4,22 @@ All notable changes to the LightNow CLI are documented here.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [1.7.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.1...v1.7.0) (2026-11-13)
8
+
9
+
10
+ ### Features
11
+
12
+ * support LibreChat local proxy configuration ([#47](https://github.com/lightnow-ai/lightnow-cli/issues/47)) ([fffb3ca](https://github.com/lightnow-ai/lightnow-cli/commit/fffb3ca2ac8c87f08e9938bfcb54e3952fcb23f7))
13
+
14
+ ## [1.6.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.0...v1.6.1) (2026-11-09)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **cli:** use active context for default favorites ([5399173](https://github.com/lightnow-ai/lightnow-cli/commit/539917321a8fbafedb05394841b5fadd0b5319e0))
20
+ * **cli:** use active context for default favorites ([1ccba54](https://github.com/lightnow-ai/lightnow-cli/commit/1ccba543e34b236d449bdedeb644029c54f49f03))
21
+ * **cli:** use active context for default favorites ([#45](https://github.com/lightnow-ai/lightnow-cli/issues/45)) ([5399173](https://github.com/lightnow-ai/lightnow-cli/commit/539917321a8fbafedb05394841b5fadd0b5319e0))
22
+
7
23
  ## [1.6.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.5.0...v1.6.0) (2026-10-09)
8
24
 
9
25
 
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: lightnow-cli
3
+ Version: 1.7.0
4
+ Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
5
+ Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
6
+ Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
7
+ Author: LightNow Team
8
+ License: Apache-2.0
9
+ License-File: LICENSE
10
+ Keywords: cli,lightnow,mcp,model-context-protocol,registry
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: filelock>=3.16.1
22
+ Requires-Dist: httpx>=0.24.0
23
+ Requires-Dist: jsonschema>=4.17.0
24
+ Requires-Dist: packaging>=24.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: pyjwt>=2.8.0
27
+ Requires-Dist: pyyaml>=6.0.0
28
+ Requires-Dist: rich>=13.0.0
29
+ Requires-Dist: truststore>=0.10.0
30
+ Requires-Dist: typer>=0.9.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: black>=23.0.0; extra == 'dev'
33
+ Requires-Dist: build>=1.2.0; extra == 'dev'
34
+ Requires-Dist: flake8>=6.0.0; extra == 'dev'
35
+ Requires-Dist: isort>=5.12.0; extra == 'dev'
36
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
37
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
39
+ Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
40
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
41
+ Requires-Dist: responses>=0.23.0; extra == 'dev'
42
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
43
+ Requires-Dist: types-jsonschema>=4.25.0; extra == 'dev'
44
+ Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
45
+ Provides-Extra: keyring
46
+ Requires-Dist: keyring>=25.6.0; extra == 'keyring'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # LightNow CLI
50
+
51
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
52
+
53
+ Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
54
+
55
+ The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
56
+ runs their connections to the servers in your Runtime profiles.
57
+
58
+ ## Get started
59
+
60
+ You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
61
+ profile with the server connections you want to use.
62
+
63
+ ### 1. Install the CLI and proxy
64
+
65
+ With Homebrew:
66
+
67
+ ```sh
68
+ brew tap lightnow-ai/tap
69
+ brew install lightnow-cli lightnow-proxy
70
+ ```
71
+
72
+ Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
73
+
74
+ ```sh
75
+ uv tool install lightnow-cli
76
+ uv tool install lightnow-proxy
77
+ ```
78
+
79
+ [pipx](https://pipx.pypa.io/stable/installation/) is also supported.
80
+
81
+ ### 2. Sign in and connect a client
82
+
83
+ If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
84
+ before syncing.
85
+
86
+ ```sh
87
+ lightnow login
88
+ lightnow sync --client codex --profile default --local-proxy
89
+ ```
90
+
91
+ Replace `default` with your profile's name. Restart the client and check its MCP
92
+ tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
93
+ Antigravity, Gemini CLI and LibreChat.
94
+
95
+ See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
96
+ for each client's setup and configuration paths.
97
+
98
+ For LibreChat, run the CLI and proxy on its backend and select its configuration:
99
+
100
+ ```sh
101
+ lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
102
+ ```
103
+
104
+ LibreChat users with access to this connection share the backend account's
105
+ LightNow permissions. Reload LibreChat after changing its configuration.
106
+
107
+ ### 3. Check the connection
108
+
109
+ ```sh
110
+ lightnow status
111
+ lightnow config-status --client codex
112
+ lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
113
+ ```
114
+
115
+ For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
116
+
117
+ ## Useful commands
118
+
119
+ | Task | Command |
120
+ | --- | --- |
121
+ | Find servers | `lightnow search sonarqube` |
122
+ | View your favorites | `lightnow favorites` |
123
+ | Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
124
+ | Check for updates | `lightnow update --check` |
125
+ | Update the CLI and proxy | `lightnow update` |
126
+
127
+ Use `lightnow --help` or `lightnow <command> --help` for available options.
128
+ For profiles, organization connections and other workflows, see the
129
+ [CLI reference](https://docs.lightnow.ai/reference/cli).
130
+
131
+ ## Contributing
132
+
133
+ See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
134
+ [release process](docs/release.md) and [security policy](SECURITY.md).
135
+
136
+ ## License
137
+
138
+ [Apache License 2.0](LICENSE).
@@ -0,0 +1,90 @@
1
+ # LightNow CLI
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/lightnow-cli.svg)](https://pypi.org/project/lightnow-cli/)
4
+
5
+ Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
6
+
7
+ The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
8
+ runs their connections to the servers in your Runtime profiles.
9
+
10
+ ## Get started
11
+
12
+ You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
13
+ profile with the server connections you want to use.
14
+
15
+ ### 1. Install the CLI and proxy
16
+
17
+ With Homebrew:
18
+
19
+ ```sh
20
+ brew tap lightnow-ai/tap
21
+ brew install lightnow-cli lightnow-proxy
22
+ ```
23
+
24
+ Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
25
+
26
+ ```sh
27
+ uv tool install lightnow-cli
28
+ uv tool install lightnow-proxy
29
+ ```
30
+
31
+ [pipx](https://pipx.pypa.io/stable/installation/) is also supported.
32
+
33
+ ### 2. Sign in and connect a client
34
+
35
+ If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
36
+ before syncing.
37
+
38
+ ```sh
39
+ lightnow login
40
+ lightnow sync --client codex --profile default --local-proxy
41
+ ```
42
+
43
+ Replace `default` with your profile's name. Restart the client and check its MCP
44
+ tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
45
+ Antigravity, Gemini CLI and LibreChat.
46
+
47
+ See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
48
+ for each client's setup and configuration paths.
49
+
50
+ For LibreChat, run the CLI and proxy on its backend and select its configuration:
51
+
52
+ ```sh
53
+ lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
54
+ ```
55
+
56
+ LibreChat users with access to this connection share the backend account's
57
+ LightNow permissions. Reload LibreChat after changing its configuration.
58
+
59
+ ### 3. Check the connection
60
+
61
+ ```sh
62
+ lightnow status
63
+ lightnow config-status --client codex
64
+ lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
65
+ ```
66
+
67
+ For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
68
+
69
+ ## Useful commands
70
+
71
+ | Task | Command |
72
+ | --- | --- |
73
+ | Find servers | `lightnow search sonarqube` |
74
+ | View your favorites | `lightnow favorites` |
75
+ | Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
76
+ | Check for updates | `lightnow update --check` |
77
+ | Update the CLI and proxy | `lightnow update` |
78
+
79
+ Use `lightnow --help` or `lightnow <command> --help` for available options.
80
+ For profiles, organization connections and other workflows, see the
81
+ [CLI reference](https://docs.lightnow.ai/reference/cli).
82
+
83
+ ## Contributing
84
+
85
+ See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
86
+ [release process](docs/release.md) and [security policy](SECURITY.md).
87
+
88
+ ## License
89
+
90
+ [Apache License 2.0](LICENSE).
@@ -0,0 +1,126 @@
1
+ """Bind a PR build to its tested checkout and one universal Python wheel."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import subprocess
8
+ import tomllib
9
+ import zipfile
10
+ from email.parser import Parser
11
+ from pathlib import Path
12
+
13
+ SCHEMA_VERSION = "platform.lightnow.ai/feature-stack-client-artifact/v1"
14
+ MANIFEST_FILENAME = "feature-stack-client-artifact.json"
15
+ COMPONENTS = {"cli": "lightnow-cli", "proxy": "lightnow-proxy"}
16
+
17
+
18
+ def build_manifest(
19
+ root: Path,
20
+ component: str,
21
+ repository: str,
22
+ pr_number: int,
23
+ head_revision: str,
24
+ checkout_revision: str,
25
+ run_id: int,
26
+ run_attempt: int,
27
+ ) -> dict:
28
+ """Validate wheel contents and runner identity before creating provenance."""
29
+ package_name = COMPONENTS.get(component)
30
+ if package_name is None or repository != f"lightnow-ai/{package_name}":
31
+ raise ValueError("Component and repository do not match")
32
+ if any(value <= 0 for value in (pr_number, run_id, run_attempt)):
33
+ raise ValueError("PR number, run ID and run attempt must be positive")
34
+ if any(
35
+ re.fullmatch(r"[0-9a-f]{40}", value) is None
36
+ for value in (head_revision, checkout_revision)
37
+ ):
38
+ raise ValueError("Head and checkout revisions must be full Git SHA-1 values")
39
+
40
+ with (root / "pyproject.toml").open("rb") as project_file:
41
+ project = tomllib.load(project_file)["project"]
42
+ if project["name"] != package_name:
43
+ raise ValueError("Repository package metadata does not match component")
44
+
45
+ wheels = sorted((root / "dist").glob("*.whl"))
46
+ if len(wheels) != 1 or wheels[0].is_symlink() or not wheels[0].is_file():
47
+ raise ValueError("Exactly one regular wheel is required")
48
+ wheel = wheels[0]
49
+ wheel_package = package_name.replace("-", "_")
50
+ expected_filename = f"{wheel_package}-{project['version']}-py3-none-any.whl"
51
+ if wheel.name != expected_filename:
52
+ raise ValueError("Expected the repository's universal py3-none-any wheel")
53
+
54
+ prefix = f"{wheel_package}-{project['version']}.dist-info/"
55
+ with zipfile.ZipFile(wheel) as archive:
56
+ for name in (f"{prefix}METADATA", f"{prefix}WHEEL"):
57
+ if archive.namelist().count(name) != 1:
58
+ raise ValueError("Wheel metadata must be unique and match its filename")
59
+ metadata_files = [
60
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
61
+ ]
62
+ if metadata_files != [f"{prefix}METADATA"]:
63
+ raise ValueError("Wheel must contain exactly one distribution's metadata")
64
+ metadata = Parser().parsestr(archive.read(f"{prefix}METADATA").decode("utf-8"))
65
+ wheel_metadata = Parser().parsestr(
66
+ archive.read(f"{prefix}WHEEL").decode("utf-8")
67
+ )
68
+ if metadata.get_all("Name") != [package_name] or metadata.get_all("Version") != [
69
+ project["version"]
70
+ ]:
71
+ raise ValueError("Wheel METADATA name/version do not match repository metadata")
72
+ if wheel_metadata.get_all("Tag") != ["py3-none-any"] or wheel_metadata.get_all(
73
+ "Root-Is-Purelib"
74
+ ) != ["true"]:
75
+ raise ValueError("Wheel must declare the universal pure-Python tag")
76
+
77
+ return {
78
+ "schemaVersion": SCHEMA_VERSION,
79
+ "component": component,
80
+ "repository": repository,
81
+ "packageName": metadata["Name"],
82
+ "packageVersion": metadata["Version"],
83
+ "filename": wheel.name,
84
+ "sha256": hashlib.sha256(wheel.read_bytes()).hexdigest(),
85
+ "source": {
86
+ "kind": "pull-request",
87
+ "number": pr_number,
88
+ "headRevision": head_revision,
89
+ "checkoutRevision": checkout_revision,
90
+ "runId": run_id,
91
+ "runAttempt": run_attempt,
92
+ "workflowPath": ".github/workflows/ci.yml",
93
+ },
94
+ }
95
+
96
+
97
+ def main() -> None:
98
+ parser = argparse.ArgumentParser(description=__doc__)
99
+ parser.add_argument("--component", choices=tuple(COMPONENTS), required=True)
100
+ parser.add_argument("--repository", required=True)
101
+ parser.add_argument("--pr-number", type=int, required=True)
102
+ parser.add_argument("--head-revision", required=True)
103
+ parser.add_argument("--run-id", type=int, required=True)
104
+ parser.add_argument("--run-attempt", type=int, required=True)
105
+ args = parser.parse_args()
106
+ root = Path(__file__).resolve().parents[1]
107
+ checkout_revision = subprocess.check_output(
108
+ ["git", "rev-parse", "HEAD"], cwd=root, text=True
109
+ ).strip()
110
+ manifest = build_manifest(
111
+ root,
112
+ args.component,
113
+ args.repository,
114
+ args.pr_number,
115
+ args.head_revision,
116
+ checkout_revision,
117
+ args.run_id,
118
+ args.run_attempt,
119
+ )
120
+ (root / "dist" / MANIFEST_FILENAME).write_text(
121
+ json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
122
+ )
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
@@ -0,0 +1,124 @@
1
+ """Offline provenance contract checks; no installation or external services."""
2
+
3
+ import hashlib
4
+ import tempfile
5
+ import unittest
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ from create_client_artifact import build_manifest
10
+
11
+
12
+ class ClientArtifactTests(unittest.TestCase):
13
+ def setUp(self) -> None:
14
+ self.directory = tempfile.TemporaryDirectory()
15
+ self.addCleanup(self.directory.cleanup)
16
+ self.root = Path(self.directory.name)
17
+ (self.root / "dist").mkdir()
18
+ (self.root / "pyproject.toml").write_text(
19
+ '[project]\nname = "lightnow-cli"\nversion = "1.2.3"\n'
20
+ )
21
+ self.wheel = self.root / "dist/lightnow_cli-1.2.3-py3-none-any.whl"
22
+ self.write_wheel()
23
+
24
+ def write_wheel(
25
+ self,
26
+ name: str = "lightnow-cli",
27
+ version: str = "1.2.3",
28
+ tag: str = "py3-none-any",
29
+ ) -> None:
30
+ with zipfile.ZipFile(self.wheel, "w") as archive:
31
+ archive.writestr(
32
+ "lightnow_cli-1.2.3.dist-info/METADATA",
33
+ f"Metadata-Version: 2.3\nName: {name}\nVersion: {version}\n",
34
+ )
35
+ archive.writestr(
36
+ "lightnow_cli-1.2.3.dist-info/WHEEL",
37
+ f"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: {tag}\n",
38
+ )
39
+
40
+ def manifest(self, **overrides: object) -> dict:
41
+ arguments = dict(
42
+ root=self.root,
43
+ component="cli",
44
+ repository="lightnow-ai/lightnow-cli",
45
+ pr_number=42,
46
+ head_revision="a" * 40,
47
+ checkout_revision="b" * 40,
48
+ run_id=123,
49
+ run_attempt=2,
50
+ )
51
+ arguments.update(overrides)
52
+ return build_manifest(**arguments)
53
+
54
+ def test_exact_contract_keeps_head_and_tested_checkout_separate(self) -> None:
55
+ self.assertEqual(
56
+ self.manifest(),
57
+ {
58
+ "schemaVersion": "platform.lightnow.ai/feature-stack-client-artifact/v1",
59
+ "component": "cli",
60
+ "repository": "lightnow-ai/lightnow-cli",
61
+ "packageName": "lightnow-cli",
62
+ "packageVersion": "1.2.3",
63
+ "filename": self.wheel.name,
64
+ "sha256": hashlib.sha256(self.wheel.read_bytes()).hexdigest(),
65
+ "source": {
66
+ "kind": "pull-request",
67
+ "number": 42,
68
+ "headRevision": "a" * 40,
69
+ "checkoutRevision": "b" * 40,
70
+ "runId": 123,
71
+ "runAttempt": 2,
72
+ "workflowPath": ".github/workflows/ci.yml",
73
+ },
74
+ },
75
+ )
76
+
77
+ def test_rejects_unbound_identity(self) -> None:
78
+ for arguments in (
79
+ {"repository": "other/lightnow-cli"},
80
+ {"component": "proxy"},
81
+ {"head_revision": "a" * 7},
82
+ {"checkout_revision": "not-a-revision"},
83
+ {"pr_number": 0},
84
+ {"run_id": -1},
85
+ {"run_attempt": 0},
86
+ ):
87
+ with self.subTest(arguments=arguments), self.assertRaises(ValueError):
88
+ self.manifest(**arguments)
89
+
90
+ def test_rejects_metadata_or_platform_mismatch(self) -> None:
91
+ for arguments in (
92
+ {"name": "another-package"},
93
+ {"version": "9.0.0"},
94
+ {"tag": "cp312-cp312-linux_x86_64"},
95
+ ):
96
+ with self.subTest(arguments=arguments):
97
+ self.write_wheel(**arguments)
98
+ with self.assertRaises(ValueError):
99
+ self.manifest()
100
+
101
+ def test_rejects_ambiguous_missing_or_symlinked_wheel(self) -> None:
102
+ extra = self.wheel.with_name("another.whl")
103
+ extra.write_bytes(self.wheel.read_bytes())
104
+ with self.assertRaises(ValueError):
105
+ self.manifest()
106
+ extra.unlink()
107
+ self.wheel.unlink()
108
+ with self.assertRaises(ValueError):
109
+ self.manifest()
110
+ self.wheel.symlink_to(self.root / "outside.whl")
111
+ with self.assertRaises(ValueError):
112
+ self.manifest()
113
+
114
+ def test_rejects_duplicate_distribution_metadata(self) -> None:
115
+ with zipfile.ZipFile(self.wheel, "a") as archive:
116
+ archive.writestr(
117
+ "other-1.0.dist-info/METADATA", "Name: other\nVersion: 1.0\n"
118
+ )
119
+ with self.assertRaises(ValueError):
120
+ self.manifest()
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main()
@@ -101,3 +101,26 @@ tag. If the project does not exist yet, use PyPI's pending publisher flow for
101
101
  ```bash
102
102
  python -m twine check dist/*
103
103
  ```
104
+
105
+ ## PR distribution provenance
106
+
107
+ PR CI archives `feature-stack-client-artifact.json` alongside the wheel and
108
+ source distribution in `python-distributions`. The manifest uses
109
+ `platform.lightnow.ai/feature-stack-client-artifact/v1` and binds the universal
110
+ wheel SHA-256 and its actual METADATA name/version to the PR number, head
111
+ revision, tested checkout revision, workflow, run ID, and run attempt. The head
112
+ and tested merge checkout are separate fields. Consumers must verify these
113
+ bindings and the wheel checksum against the selected successful CI run before
114
+ installing a candidate; the manifest itself is build metadata, not a signature
115
+ or a runtime test result.
116
+
117
+ The generator uses only Python's standard library. After a clean package build,
118
+ run its focused offline contract checks with:
119
+
120
+ ```bash
121
+ python -m unittest discover -s ci -p 'test_*.py'
122
+ ```
123
+
124
+ Main CI does not fabricate PR metadata. Tagged release publishing is unchanged:
125
+ release consumers qualify the exact Git tag and published PyPI wheel separately.
126
+ The PR-only JSON never enters the existing PyPI release upload.
@@ -1,3 +1,3 @@
1
1
  """LightNow CLI - A command-line tool for managing MCP servers."""
2
2
 
3
- __version__ = "1.6.0" # x-release-please-version
3
+ __version__ = "1.7.0" # x-release-please-version