substrate-setup 0.3.1__tar.gz → 0.4.1__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.
- substrate_setup-0.4.1/CHANGELOG.md +46 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/PKG-INFO +17 -8
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/README.md +8 -4
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/pyproject.toml +22 -5
- substrate_setup-0.4.1/substrate_setup/__init__.py +14 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/aider.py +14 -2
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/base.py +14 -1
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/claude_code.py +14 -2
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/codex.py +17 -5
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/continue_dev.py +14 -2
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/cursor.py +14 -2
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/hermes.py +14 -2
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/credentials.py +3 -3
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/data/fallback_catalog.json +5 -5
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/env_persist.py +37 -21
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_claude_code.py +4 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_codex.py +4 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_cursor.py +13 -1
- substrate_setup-0.4.1/tests/conftest.py +37 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_cli.py +61 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_env_persist.py +30 -0
- substrate_setup-0.4.1/uv.lock +866 -0
- substrate_setup-0.3.1/CHANGELOG.md +0 -22
- substrate_setup-0.3.1/substrate_setup/__init__.py +0 -6
- substrate_setup-0.3.1/tests/conftest.py +0 -19
- substrate_setup-0.3.1/uv.lock +0 -352
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/.gitignore +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/scripts/lint_no_app_import.sh +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/scripts/regenerate_fallback_catalog.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/__main__.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/agents/__init__.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/backup.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/catalog.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/cli.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/substrate_setup/markers.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/__init__.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/__init__.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_aider.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_continue_dev.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/agents/test_hermes.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_backup.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_catalog.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_credentials.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_e2e.py +0 -0
- {substrate_setup-0.3.1 → substrate_setup-0.4.1}/tests/test_markers.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.1 — 2026-05-30
|
|
4
|
+
|
|
5
|
+
- Fix `substrate-setup --version` reporting a stale number. `__version__` is
|
|
6
|
+
now derived from the installed package metadata
|
|
7
|
+
(`importlib.metadata.version`) instead of a hand-maintained string, so it
|
|
8
|
+
can never drift from `pyproject.toml` again. (0.4.0 shipped with a leftover
|
|
9
|
+
`__version__ = "0.3.1"`; the compatibility fix itself was unaffected.)
|
|
10
|
+
|
|
11
|
+
## 0.4.0 — 2026-05-30
|
|
12
|
+
|
|
13
|
+
- Lower the supported Python floor from **3.12 to 3.8**. Nothing in the tool
|
|
14
|
+
actually required 3.12 — the gate was conservative. `pip install
|
|
15
|
+
substrate-setup` now works on Anaconda's default 3.9 and other older
|
|
16
|
+
interpreters, no `uv`/`pipx` workaround needed.
|
|
17
|
+
- `enum.StrEnum` (3.11+) imports natively on 3.11+ with a minimal
|
|
18
|
+
`(str, Enum)` backport on older interpreters.
|
|
19
|
+
- The `tomllib` → `tomli` fallback is now `sys.version_info`-gated, and
|
|
20
|
+
`tomli` is a declared dependency (`python_version < '3.11'`) — previously
|
|
21
|
+
the fallback path had no dependency backing it and would have failed.
|
|
22
|
+
- `tomli-w` lower bound relaxed `1.2 → 1.0` so 3.8 resolves.
|
|
23
|
+
- No CLI or config-schema changes — pure compatibility widening. The
|
|
24
|
+
test/type tooling still requires 3.9+ (`pytest-httpx`/`mypy` dropped 3.8);
|
|
25
|
+
end users on 3.8 are unaffected.
|
|
26
|
+
|
|
27
|
+
## 0.3.1 — 2026-05-24
|
|
28
|
+
|
|
29
|
+
- Persist `SUBSTRATE_API_KEY` automatically during `configure`:
|
|
30
|
+
on Windows, writes `HKCU\Environment\SUBSTRATE_API_KEY` via `winreg`
|
|
31
|
+
+ `WM_SETTINGCHANGE` broadcast — visible to GUI agents (Codex Desktop)
|
|
32
|
+
on next launch without the user editing rc files or restarting their
|
|
33
|
+
session. On macOS / Linux, appends a marker-fenced `export` block (or
|
|
34
|
+
fish-syntax `set -gx` for fish users) to the user's shell rc. Re-runs
|
|
35
|
+
collapse stale blocks idempotently — the rc never grows on repeated
|
|
36
|
+
configures.
|
|
37
|
+
- Codex adapter now prints a one-line Windows Defender exclusion command
|
|
38
|
+
to eliminate SmartScreen prompts on per-action helper binary spawns.
|
|
39
|
+
Informational only — substrate-setup never elevates itself.
|
|
40
|
+
- Adapters surface env-persistence failures with a clear `WARNING:`
|
|
41
|
+
prefix + manual fallback hint so users notice when the convenience
|
|
42
|
+
step fails but the config file itself was written successfully.
|
|
43
|
+
- Codex adapter drops the stale "/v1/responses NOT YET STARTED" warning
|
|
44
|
+
— Phase 2 shipped on 2026-05-24.
|
|
45
|
+
- No config schema changes from 0.3.0. Upgrade cleanly via
|
|
46
|
+
`uv tool install --force substrate-setup` (or pipx).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: substrate-setup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: One-shot local configurator for coding agents against a Substrate gateway
|
|
5
5
|
Project-URL: Homepage, https://github.com/FrankXiaA/substrate-solutions
|
|
6
6
|
Project-URL: Source, https://github.com/FrankXiaA/substrate-solutions/tree/main/substrate-api/substrate_setup
|
|
@@ -14,16 +14,21 @@ Classifier: Intended Audience :: Developers
|
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
22
|
Classifier: Topic :: Software Development :: Code Generators
|
|
19
23
|
Classifier: Topic :: Utilities
|
|
20
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.8
|
|
21
25
|
Requires-Dist: httpx>=0.27
|
|
22
26
|
Requires-Dist: ruamel-yaml>=0.18
|
|
23
|
-
Requires-Dist: tomli-w>=1.
|
|
27
|
+
Requires-Dist: tomli-w>=1.0
|
|
28
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
24
29
|
Provides-Extra: dev
|
|
25
30
|
Requires-Dist: mypy<2,>=1.13; extra == 'dev'
|
|
26
|
-
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-httpx>=0.30; (python_version >= '3.9') and extra == 'dev'
|
|
27
32
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
33
|
Requires-Dist: ruff<1,>=0.7; extra == 'dev'
|
|
29
34
|
Description-Content-Type: text/markdown
|
|
@@ -34,10 +39,16 @@ One-shot configurator that points local coding agents at a Substrate gateway.
|
|
|
34
39
|
|
|
35
40
|
## Install
|
|
36
41
|
|
|
37
|
-
`substrate-setup`
|
|
42
|
+
`substrate-setup` runs on **Python 3.8 or newer** — including Anaconda's default 3.9 and the system Python on older Linux/macOS boxes. Plain `pip` works:
|
|
38
43
|
|
|
39
44
|
```bash
|
|
40
|
-
|
|
45
|
+
pip install substrate-setup
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you'd rather keep the tool in its own isolated environment (recommended, so it can't clash with a project's dependencies), use `uv` or `pipx`:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Option A — uv (also downloads a Python for the tool if needed)
|
|
41
52
|
uv tool install substrate-setup
|
|
42
53
|
|
|
43
54
|
# Option B — pipx
|
|
@@ -52,8 +63,6 @@ curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux
|
|
|
52
63
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
> **Why not `pip install substrate-setup`?** It works only if the `pip` on your PATH is bound to a Python 3.12+ interpreter. Anaconda's default `pip` (Python 3.9) is the common pitfall — PyPI hides every release from it with `Requires-Python >=3.12` and the error message is unhelpful. `uv` and `pipx` both create an isolated 3.12 venv for the tool, so they sidestep the issue entirely.
|
|
56
|
-
|
|
57
66
|
## Use
|
|
58
67
|
|
|
59
68
|
```bash
|
|
@@ -4,10 +4,16 @@ One-shot configurator that points local coding agents at a Substrate gateway.
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
`substrate-setup`
|
|
7
|
+
`substrate-setup` runs on **Python 3.8 or newer** — including Anaconda's default 3.9 and the system Python on older Linux/macOS boxes. Plain `pip` works:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
pip install substrate-setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If you'd rather keep the tool in its own isolated environment (recommended, so it can't clash with a project's dependencies), use `uv` or `pipx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Option A — uv (also downloads a Python for the tool if needed)
|
|
11
17
|
uv tool install substrate-setup
|
|
12
18
|
|
|
13
19
|
# Option B — pipx
|
|
@@ -22,8 +28,6 @@ curl -LsSf https://astral.sh/uv/install.sh | sh # macOS / Linux
|
|
|
22
28
|
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
> **Why not `pip install substrate-setup`?** It works only if the `pip` on your PATH is bound to a Python 3.12+ interpreter. Anaconda's default `pip` (Python 3.9) is the common pitfall — PyPI hides every release from it with `Requires-Python >=3.12` and the error message is unhelpful. `uv` and `pipx` both create an isolated 3.12 venv for the tool, so they sidestep the issue entirely.
|
|
26
|
-
|
|
27
31
|
## Use
|
|
28
32
|
|
|
29
33
|
```bash
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "substrate-setup"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "One-shot local configurator for coding agents against a Substrate gateway"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
11
|
authors = [{ name = "Substrate Solutions" }]
|
|
12
12
|
license = { text = "MIT" }
|
|
13
13
|
keywords = ["substrate", "llm", "gateway", "aider", "continue", "hermes", "openai-compatible"]
|
|
@@ -18,6 +18,10 @@ classifiers = [
|
|
|
18
18
|
"License :: OSI Approved :: MIT License",
|
|
19
19
|
"Operating System :: OS Independent",
|
|
20
20
|
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
21
25
|
"Programming Language :: Python :: 3.12",
|
|
22
26
|
"Topic :: Software Development :: Code Generators",
|
|
23
27
|
"Topic :: Utilities",
|
|
@@ -25,7 +29,11 @@ classifiers = [
|
|
|
25
29
|
dependencies = [
|
|
26
30
|
"httpx>=0.27",
|
|
27
31
|
"ruamel.yaml>=0.18",
|
|
28
|
-
"tomli-w>=1.
|
|
32
|
+
"tomli-w>=1.0",
|
|
33
|
+
# Reading TOML (codex config, credentials probe) uses the stdlib
|
|
34
|
+
# ``tomllib`` on 3.11+; older interpreters fall back to the ``tomli``
|
|
35
|
+
# backport. See ``credentials.py`` / ``agents/codex.py`` import guards.
|
|
36
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
29
37
|
]
|
|
30
38
|
|
|
31
39
|
[project.urls]
|
|
@@ -34,9 +42,12 @@ Source = "https://github.com/FrankXiaA/substrate-solutions/tree/main/substrate-a
|
|
|
34
42
|
Issues = "https://github.com/FrankXiaA/substrate-solutions/issues"
|
|
35
43
|
|
|
36
44
|
[project.optional-dependencies]
|
|
45
|
+
# Runtime floor is Python 3.8, but the test/type tooling needs 3.9+
|
|
46
|
+
# (pytest-httpx>=0.30 and modern mypy both dropped 3.8). Developing/testing
|
|
47
|
+
# the package therefore requires 3.9+; end users on 3.8 are unaffected.
|
|
37
48
|
dev = [
|
|
38
49
|
"pytest>=8.0",
|
|
39
|
-
"pytest-httpx>=0.30",
|
|
50
|
+
"pytest-httpx>=0.30; python_version >= '3.9'",
|
|
40
51
|
"ruff>=0.7,<1",
|
|
41
52
|
"mypy>=1.13,<2",
|
|
42
53
|
]
|
|
@@ -50,8 +61,14 @@ packages = ["substrate_setup"]
|
|
|
50
61
|
[tool.pytest.ini_options]
|
|
51
62
|
testpaths = ["tests"]
|
|
52
63
|
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
target-version = "py38"
|
|
66
|
+
|
|
53
67
|
[tool.mypy]
|
|
54
|
-
|
|
68
|
+
# Lowest version mypy itself supports as an analysis target; the package's
|
|
69
|
+
# runtime floor is 3.8 (see requires-python). 3.8-only breakage is caught by
|
|
70
|
+
# the test suite + import smoke, not the type checker.
|
|
71
|
+
python_version = "3.9"
|
|
55
72
|
strict = true
|
|
56
73
|
ignore_missing_imports = true
|
|
57
74
|
packages = ["substrate_setup"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""substrate-setup — one-shot configurator for coding agents.
|
|
2
|
+
|
|
3
|
+
See https://github.com/FrankXiaA/substrate-solutions for the gateway it
|
|
4
|
+
configures against.
|
|
5
|
+
"""
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
# Single source of truth: read the version from the installed package
|
|
10
|
+
# metadata (driven by pyproject.toml) so it can never drift from the
|
|
11
|
+
# published release. ``importlib.metadata`` is stdlib since Python 3.8.
|
|
12
|
+
__version__ = version("substrate-setup")
|
|
13
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -56,7 +56,12 @@ from substrate_setup.agents.base import (
|
|
|
56
56
|
ResultStatus,
|
|
57
57
|
)
|
|
58
58
|
from substrate_setup.backup import backup_once
|
|
59
|
-
from substrate_setup.env_persist import
|
|
59
|
+
from substrate_setup.env_persist import (
|
|
60
|
+
PersistResult,
|
|
61
|
+
PersistStatus,
|
|
62
|
+
format_persist_message_for_user,
|
|
63
|
+
persist_substrate_api_key,
|
|
64
|
+
)
|
|
60
65
|
|
|
61
66
|
MANAGED_KEYS_MARKER = "_substrate_setup_managed_keys"
|
|
62
67
|
MANAGED_KEYS: list[str] = ["openai-api-base", "openai-api-key", "model"]
|
|
@@ -262,7 +267,14 @@ class AiderAgent(Agent):
|
|
|
262
267
|
)
|
|
263
268
|
_restrict_file_mode(meta_path)
|
|
264
269
|
|
|
265
|
-
|
|
270
|
+
if not ctx.dry_run:
|
|
271
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
272
|
+
else:
|
|
273
|
+
persist_result = PersistResult(
|
|
274
|
+
status=PersistStatus.SKIPPED,
|
|
275
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
276
|
+
path=None,
|
|
277
|
+
)
|
|
266
278
|
print(format_persist_message_for_user(persist_result))
|
|
267
279
|
|
|
268
280
|
return AgentResult(
|
|
@@ -5,11 +5,24 @@ interface. The orchestrator in cli.py treats them polymorphically.
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import sys
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
|
-
from enum import StrEnum
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
if sys.version_info >= (3, 11):
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
else: # pragma: no cover - Python <3.11 backport
|
|
16
|
+
# ``enum.StrEnum`` landed in 3.11. On older interpreters a plain
|
|
17
|
+
# ``(str, Enum)`` mixin gives the same behaviour: members are real
|
|
18
|
+
# ``str`` instances, so ``ResultStatus.SUCCESS == "success"`` holds.
|
|
19
|
+
# ``__str__ = str.__str__`` matches 3.11's StrEnum, where ``str(member)``
|
|
20
|
+
# returns the bare value rather than ``"ResultStatus.SUCCESS"``.
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
class StrEnum(str, Enum):
|
|
24
|
+
__str__ = str.__str__
|
|
25
|
+
|
|
13
26
|
from substrate_setup.catalog import CatalogEntry
|
|
14
27
|
|
|
15
28
|
|
|
@@ -44,7 +44,12 @@ from substrate_setup.agents.base import (
|
|
|
44
44
|
ResultStatus,
|
|
45
45
|
)
|
|
46
46
|
from substrate_setup.backup import backup_once
|
|
47
|
-
from substrate_setup.env_persist import
|
|
47
|
+
from substrate_setup.env_persist import (
|
|
48
|
+
PersistResult,
|
|
49
|
+
PersistStatus,
|
|
50
|
+
format_persist_message_for_user,
|
|
51
|
+
persist_substrate_api_key,
|
|
52
|
+
)
|
|
48
53
|
|
|
49
54
|
MANAGED_ENV_KEYS_MARKER = "_substrate_setup_managed_env_keys"
|
|
50
55
|
MANAGED_ENV_KEYS: list[str] = [
|
|
@@ -219,7 +224,14 @@ class ClaudeCodeAgent(Agent):
|
|
|
219
224
|
)
|
|
220
225
|
_restrict_file_mode(settings_path)
|
|
221
226
|
|
|
222
|
-
|
|
227
|
+
if not ctx.dry_run:
|
|
228
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
229
|
+
else:
|
|
230
|
+
persist_result = PersistResult(
|
|
231
|
+
status=PersistStatus.SKIPPED,
|
|
232
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
233
|
+
path=None,
|
|
234
|
+
)
|
|
223
235
|
bullets = "\n".join(f" - {m.id}" for m in chat_models)
|
|
224
236
|
print(
|
|
225
237
|
"Claude Code configured to use the Substrate gateway.\n"
|
|
@@ -42,10 +42,10 @@ from typing import Any
|
|
|
42
42
|
|
|
43
43
|
import tomli_w
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
if sys.version_info >= (3, 11):
|
|
46
46
|
import tomllib
|
|
47
|
-
|
|
48
|
-
import tomli as tomllib
|
|
47
|
+
else: # pragma: no cover — Python <3.11 uses the tomli backport
|
|
48
|
+
import tomli as tomllib
|
|
49
49
|
|
|
50
50
|
from substrate_setup.agents.base import (
|
|
51
51
|
Agent,
|
|
@@ -54,7 +54,12 @@ from substrate_setup.agents.base import (
|
|
|
54
54
|
ResultStatus,
|
|
55
55
|
)
|
|
56
56
|
from substrate_setup.backup import backup_once
|
|
57
|
-
from substrate_setup.env_persist import
|
|
57
|
+
from substrate_setup.env_persist import (
|
|
58
|
+
PersistResult,
|
|
59
|
+
PersistStatus,
|
|
60
|
+
format_persist_message_for_user,
|
|
61
|
+
persist_substrate_api_key,
|
|
62
|
+
)
|
|
58
63
|
|
|
59
64
|
PROVIDER_KEY = "substrate"
|
|
60
65
|
ENV_KEY_NAME = "SUBSTRATE_API_KEY"
|
|
@@ -217,7 +222,14 @@ class CodexAgent(Agent):
|
|
|
217
222
|
cfg_path.write_text(f"{MARKER_COMMENT}\n{body}", encoding="utf-8")
|
|
218
223
|
_restrict_file_mode(cfg_path)
|
|
219
224
|
|
|
220
|
-
|
|
225
|
+
if not ctx.dry_run:
|
|
226
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
227
|
+
else:
|
|
228
|
+
persist_result = PersistResult(
|
|
229
|
+
status=PersistStatus.SKIPPED,
|
|
230
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
231
|
+
path=None,
|
|
232
|
+
)
|
|
221
233
|
bullets = "\n".join(f" - {m.id}" for m in chat_models)
|
|
222
234
|
print(
|
|
223
235
|
"Codex configured to use the Substrate gateway.\n"
|
|
@@ -46,7 +46,12 @@ from substrate_setup.agents.base import (
|
|
|
46
46
|
ResultStatus,
|
|
47
47
|
)
|
|
48
48
|
from substrate_setup.backup import backup_once
|
|
49
|
-
from substrate_setup.env_persist import
|
|
49
|
+
from substrate_setup.env_persist import (
|
|
50
|
+
PersistResult,
|
|
51
|
+
PersistStatus,
|
|
52
|
+
format_persist_message_for_user,
|
|
53
|
+
persist_substrate_api_key,
|
|
54
|
+
)
|
|
50
55
|
from substrate_setup.catalog import CatalogEntry
|
|
51
56
|
from substrate_setup.markers import MARKER_KEY, MARKER_VALUE, is_substrate_managed
|
|
52
57
|
|
|
@@ -241,7 +246,14 @@ class ContinueAgent(Agent):
|
|
|
241
246
|
if not ctx.dry_run:
|
|
242
247
|
_dump_yaml(yaml_path, payload)
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
if not ctx.dry_run:
|
|
250
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
251
|
+
else:
|
|
252
|
+
persist_result = PersistResult(
|
|
253
|
+
status=PersistStatus.SKIPPED,
|
|
254
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
255
|
+
path=None,
|
|
256
|
+
)
|
|
245
257
|
print(format_persist_message_for_user(persist_result))
|
|
246
258
|
|
|
247
259
|
msg = f"{len(ctx.catalog)} models written, API base + key set in {yaml_path}."
|
|
@@ -17,7 +17,12 @@ from substrate_setup.agents.base import (
|
|
|
17
17
|
ConfigureContext,
|
|
18
18
|
ResultStatus,
|
|
19
19
|
)
|
|
20
|
-
from substrate_setup.env_persist import
|
|
20
|
+
from substrate_setup.env_persist import (
|
|
21
|
+
PersistResult,
|
|
22
|
+
PersistStatus,
|
|
23
|
+
format_persist_message_for_user,
|
|
24
|
+
persist_substrate_api_key,
|
|
25
|
+
)
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
def _user_data_candidates() -> list[Path]:
|
|
@@ -44,7 +49,14 @@ class CursorAgent(Agent):
|
|
|
44
49
|
return None
|
|
45
50
|
|
|
46
51
|
def configure(self, ctx: ConfigureContext) -> AgentResult:
|
|
47
|
-
|
|
52
|
+
if not ctx.dry_run:
|
|
53
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
54
|
+
else:
|
|
55
|
+
persist_result = PersistResult(
|
|
56
|
+
status=PersistStatus.SKIPPED,
|
|
57
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
58
|
+
path=None,
|
|
59
|
+
)
|
|
48
60
|
chat_models = [
|
|
49
61
|
e for e in ctx.catalog
|
|
50
62
|
if e.output_modality == "text"
|
|
@@ -62,7 +62,12 @@ from substrate_setup.agents.base import (
|
|
|
62
62
|
ResultStatus,
|
|
63
63
|
)
|
|
64
64
|
from substrate_setup.backup import backup_once
|
|
65
|
-
from substrate_setup.env_persist import
|
|
65
|
+
from substrate_setup.env_persist import (
|
|
66
|
+
PersistResult,
|
|
67
|
+
PersistStatus,
|
|
68
|
+
format_persist_message_for_user,
|
|
69
|
+
persist_substrate_api_key,
|
|
70
|
+
)
|
|
66
71
|
|
|
67
72
|
# Top-level marker key listing the model.* keys substrate-setup owns.
|
|
68
73
|
MANAGED_KEYS_MARKER = "_substrate_setup_managed_keys"
|
|
@@ -387,7 +392,14 @@ class HermesAgent(Agent):
|
|
|
387
392
|
if not ctx.dry_run:
|
|
388
393
|
_dump_yaml(cfg_path, payload)
|
|
389
394
|
|
|
390
|
-
|
|
395
|
+
if not ctx.dry_run:
|
|
396
|
+
persist_result = persist_substrate_api_key(ctx.api_key)
|
|
397
|
+
else:
|
|
398
|
+
persist_result = PersistResult(
|
|
399
|
+
status=PersistStatus.SKIPPED,
|
|
400
|
+
message="(dry-run) would persist SUBSTRATE_API_KEY",
|
|
401
|
+
path=None,
|
|
402
|
+
)
|
|
391
403
|
print(format_persist_message_for_user(persist_result))
|
|
392
404
|
|
|
393
405
|
msg = (
|
|
@@ -8,10 +8,10 @@ from collections.abc import Callable
|
|
|
8
8
|
from getpass import getpass
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
12
|
import tomllib
|
|
13
|
-
|
|
14
|
-
import tomli as tomllib
|
|
13
|
+
else: # pragma: no cover - Python <3.11 uses the tomli backport
|
|
14
|
+
import tomli as tomllib
|
|
15
15
|
|
|
16
16
|
KEY_REGEX = re.compile(r"^sk-substrate-[A-Za-z0-9_-]{20,}$")
|
|
17
17
|
ENV_VAR = "SUBSTRATE_API_KEY"
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"object": "list",
|
|
3
3
|
"data": [
|
|
4
4
|
{
|
|
5
|
-
"id": "claude-opus-4-
|
|
5
|
+
"id": "claude-opus-4-8",
|
|
6
6
|
"object": "model",
|
|
7
|
-
"created":
|
|
7
|
+
"created": 1779926400,
|
|
8
8
|
"owned_by": "anthropic",
|
|
9
|
-
"display_name": "Claude Opus 4.
|
|
9
|
+
"display_name": "Claude Opus 4.8",
|
|
10
10
|
"description": "Anthropic flagship (most capable, agentic coding)",
|
|
11
|
-
"context_length":
|
|
11
|
+
"context_length": 1000000,
|
|
12
12
|
"max_completion_tokens": 32000,
|
|
13
13
|
"pricing": {
|
|
14
14
|
"input_per_million_usd": 5.0,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"owned_by": "anthropic",
|
|
30
30
|
"display_name": "Claude Sonnet 4.6",
|
|
31
31
|
"description": "Balanced quality and speed",
|
|
32
|
-
"context_length":
|
|
32
|
+
"context_length": 1000000,
|
|
33
33
|
"max_completion_tokens": 32000,
|
|
34
34
|
"pricing": {
|
|
35
35
|
"input_per_million_usd": 3.0,
|
|
@@ -25,6 +25,7 @@ import sys
|
|
|
25
25
|
from dataclasses import dataclass
|
|
26
26
|
from enum import Enum
|
|
27
27
|
from pathlib import Path
|
|
28
|
+
from typing import Any, cast
|
|
28
29
|
|
|
29
30
|
_ENV_VAR = "SUBSTRATE_API_KEY"
|
|
30
31
|
_MARKER_OPEN = "# >>> substrate-setup managed >>>"
|
|
@@ -89,14 +90,15 @@ def _persist_windows(api_key: str) -> PersistResult:
|
|
|
89
90
|
path=None,
|
|
90
91
|
)
|
|
91
92
|
|
|
93
|
+
wr = cast(Any, winreg) # cross-platform mypy: winreg attrs absent from Linux stubs
|
|
92
94
|
try:
|
|
93
|
-
with
|
|
94
|
-
|
|
95
|
+
with wr.OpenKey(
|
|
96
|
+
wr.HKEY_CURRENT_USER,
|
|
95
97
|
"Environment",
|
|
96
98
|
0,
|
|
97
|
-
|
|
99
|
+
wr.KEY_SET_VALUE,
|
|
98
100
|
) as h:
|
|
99
|
-
|
|
101
|
+
wr.SetValueEx(h, _ENV_VAR, 0, wr.REG_EXPAND_SZ, api_key)
|
|
100
102
|
except OSError as exc:
|
|
101
103
|
return PersistResult(
|
|
102
104
|
status=PersistStatus.ERROR,
|
|
@@ -130,8 +132,10 @@ def _broadcast_setting_change_windows() -> None:
|
|
|
130
132
|
HWND_BROADCAST = 0xFFFF
|
|
131
133
|
WM_SETTINGCHANGE = 0x001A
|
|
132
134
|
SMTO_ABORTIFHUNG = 0x0002
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
ct = cast(Any, ctypes) # cross-platform mypy: WinDLL absent from Linux stubs
|
|
136
|
+
wt = cast(Any, wintypes) # cross-platform mypy: wintypes attrs absent from Linux stubs
|
|
137
|
+
user32 = ct.WinDLL("user32", use_last_error=True)
|
|
138
|
+
result = wt.DWORD()
|
|
135
139
|
try:
|
|
136
140
|
user32.SendMessageTimeoutW(
|
|
137
141
|
HWND_BROADCAST,
|
|
@@ -140,7 +144,7 @@ def _broadcast_setting_change_windows() -> None:
|
|
|
140
144
|
"Environment",
|
|
141
145
|
SMTO_ABORTIFHUNG,
|
|
142
146
|
5000,
|
|
143
|
-
|
|
147
|
+
ct.byref(result),
|
|
144
148
|
)
|
|
145
149
|
except OSError:
|
|
146
150
|
pass # broadcast is best-effort
|
|
@@ -170,20 +174,32 @@ def _persist_posix(api_key: str) -> PersistResult:
|
|
|
170
174
|
export_line = f'export {_ENV_VAR}="{api_key}"'
|
|
171
175
|
block = f"{_MARKER_OPEN}\n{export_line}\n{_MARKER_CLOSE}\n"
|
|
172
176
|
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
177
|
+
# Wrap the rc read/write in try/except — mirror the Windows path's
|
|
178
|
+
# OSError handling. Without this, an unreadable file (binary or bad
|
|
179
|
+
# encoding) or an unwritable mount (read-only filesystem, permissions
|
|
180
|
+
# error) crashes configure with an unhandled exception instead of
|
|
181
|
+
# returning a clean ERROR result the orchestrator can surface.
|
|
182
|
+
try:
|
|
183
|
+
# Ensure parent dir exists — fish users on a fresh install won't
|
|
184
|
+
# have ~/.config/fish/ yet.
|
|
185
|
+
rc.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
existing = rc.read_text(encoding="utf-8") if rc.exists() else ""
|
|
187
|
+
|
|
188
|
+
# Strip ALL existing marker blocks (defensive: a buggy earlier run
|
|
189
|
+
# or hand-editing could leave duplicates) and append exactly one new
|
|
190
|
+
# block. Invariant: exactly one marker block after any call.
|
|
191
|
+
stripped = _MARKER_BLOCK_RE.sub("", existing)
|
|
192
|
+
if stripped and not stripped.endswith("\n"):
|
|
193
|
+
stripped += "\n"
|
|
194
|
+
new_body = stripped + block
|
|
195
|
+
|
|
196
|
+
rc.write_text(new_body, encoding="utf-8")
|
|
197
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
198
|
+
return PersistResult(
|
|
199
|
+
status=PersistStatus.ERROR,
|
|
200
|
+
message=f"Failed to write {rc}: {exc}",
|
|
201
|
+
path=rc,
|
|
202
|
+
)
|
|
187
203
|
|
|
188
204
|
return PersistResult(
|
|
189
205
|
status=PersistStatus.PERSISTED,
|
|
@@ -169,6 +169,10 @@ def test_configure_handles_malformed_json(home_dir):
|
|
|
169
169
|
assert "valid JSON" in result.message
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
@pytest.mark.skipif(
|
|
173
|
+
sys.platform == "win32",
|
|
174
|
+
reason="POSIX file modes don't apply on Windows (NTFS reports 0o666)",
|
|
175
|
+
)
|
|
172
176
|
def test_configure_chmods_0600_on_posix(home_dir, monkeypatch):
|
|
173
177
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
174
178
|
ClaudeCodeAgent().configure(_ctx())
|
|
@@ -181,6 +181,10 @@ def test_configure_returns_clean_error_on_malformed_toml(home_dir):
|
|
|
181
181
|
assert "valid TOML" in result.message
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
@pytest.mark.skipif(
|
|
185
|
+
sys.platform == "win32",
|
|
186
|
+
reason="POSIX file modes don't apply on Windows (NTFS reports 0o666)",
|
|
187
|
+
)
|
|
184
188
|
def test_configure_chmods_0600_on_posix(home_dir, monkeypatch):
|
|
185
189
|
monkeypatch.setattr(sys, "platform", "linux")
|
|
186
190
|
CodexAgent().configure(_ctx())
|
|
@@ -125,7 +125,19 @@ def test_cursor_configure_calls_env_persist(monkeypatch, tmp_path):
|
|
|
125
125
|
assert captured == [VALID_KEY]
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def test_walkthrough_excludes_image_gen_models(capsys):
|
|
128
|
+
def test_walkthrough_excludes_image_gen_models(monkeypatch, capsys):
|
|
129
|
+
# Patch persist at the call site — without this, configure() on a
|
|
130
|
+
# Windows test host writes SUBSTRATE_API_KEY to the real
|
|
131
|
+
# HKCU\Environment hive (no $HOME redirection in scope here).
|
|
132
|
+
monkeypatch.setattr(
|
|
133
|
+
"substrate_setup.agents.cursor.persist_substrate_api_key",
|
|
134
|
+
lambda key: PersistResult(
|
|
135
|
+
status=PersistStatus.PERSISTED,
|
|
136
|
+
message="patched in test",
|
|
137
|
+
path=None,
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
129
141
|
chat = CatalogEntry(
|
|
130
142
|
id="openai/gpt-5.5", provider="openai",
|
|
131
143
|
display_name="GPT-5.5", description="...",
|