bayafs 0.1.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 (40) hide show
  1. bayafs-0.1.0/.gitignore +223 -0
  2. bayafs-0.1.0/LICENSE +9 -0
  3. bayafs-0.1.0/PKG-INFO +109 -0
  4. bayafs-0.1.0/README.md +84 -0
  5. bayafs-0.1.0/pyproject.toml +50 -0
  6. bayafs-0.1.0/src/baya/__init__.py +11 -0
  7. bayafs-0.1.0/src/baya/broker.py +178 -0
  8. bayafs-0.1.0/src/baya/cli.py +71 -0
  9. bayafs-0.1.0/src/baya/cluster.py +286 -0
  10. bayafs-0.1.0/src/baya/commands/__init__.py +1 -0
  11. bayafs-0.1.0/src/baya/commands/config.py +55 -0
  12. bayafs-0.1.0/src/baya/commands/install.py +200 -0
  13. bayafs-0.1.0/src/baya/commands/license.py +89 -0
  14. bayafs-0.1.0/src/baya/commands/lifecycle.py +414 -0
  15. bayafs-0.1.0/src/baya/commands/release.py +71 -0
  16. bayafs-0.1.0/src/baya/commands/user.py +262 -0
  17. bayafs-0.1.0/src/baya/config.py +135 -0
  18. bayafs-0.1.0/src/baya/doctor.py +376 -0
  19. bayafs-0.1.0/src/baya/errors.py +17 -0
  20. bayafs-0.1.0/src/baya/gateway.py +110 -0
  21. bayafs-0.1.0/src/baya/journal.py +90 -0
  22. bayafs-0.1.0/src/baya/keycloak.py +176 -0
  23. bayafs-0.1.0/src/baya/license.py +62 -0
  24. bayafs-0.1.0/src/baya/prereqs.py +173 -0
  25. bayafs-0.1.0/src/baya/release.py +144 -0
  26. bayafs-0.1.0/src/baya/shell.py +70 -0
  27. bayafs-0.1.0/src/baya/tls.py +98 -0
  28. bayafs-0.1.0/src/baya/ui.py +176 -0
  29. bayafs-0.1.0/src/baya/users.py +153 -0
  30. bayafs-0.1.0/src/baya/verify.py +200 -0
  31. bayafs-0.1.0/src/baya/workspace.py +159 -0
  32. bayafs-0.1.0/tests/conftest.py +39 -0
  33. bayafs-0.1.0/tests/test_broker.py +144 -0
  34. bayafs-0.1.0/tests/test_cli_flow.py +116 -0
  35. bayafs-0.1.0/tests/test_config.py +75 -0
  36. bayafs-0.1.0/tests/test_doctor.py +122 -0
  37. bayafs-0.1.0/tests/test_license.py +39 -0
  38. bayafs-0.1.0/tests/test_release.py +146 -0
  39. bayafs-0.1.0/tests/test_users.py +106 -0
  40. bayafs-0.1.0/tests/test_workspace_and_gateway.py +98 -0
@@ -0,0 +1,223 @@
1
+ # Prerequisites
2
+ *.d
3
+
4
+ # Compiled Object files
5
+ *.slo
6
+ *.lo
7
+ *.o
8
+ *.obj
9
+
10
+ # Precompiled Headers
11
+ *.gch
12
+ *.pch
13
+
14
+ # Compiled Dynamic libraries
15
+ *.so
16
+ *.dylib
17
+ *.dll
18
+
19
+ # Fortran module files
20
+ *.mod
21
+ *.smod
22
+
23
+ # Compiled Static libraries
24
+ *.lai
25
+ *.la
26
+ *.a
27
+ *.lib
28
+
29
+ # Executables
30
+ *.exe
31
+ *.out
32
+ *.app
33
+
34
+ # python virtual env
35
+ env/
36
+ .venv/
37
+
38
+ # temp directory for cmake builds
39
+ build/
40
+
41
+ # ctest scratch, written into the cwd when ctest runs from the repo root.
42
+ # Anchored: a nested Testing/ elsewhere in the tree is real source.
43
+ /Testing/
44
+
45
+ # pyarmor license registration zip (required for release generation)
46
+ generate_release/fabric_studio/dependencies/pyarmor-regfile-4186.zip
47
+
48
+ # temp directories for release generation and cmake builds
49
+ /release_scratch/
50
+ /release/
51
+ /generated_rtl/
52
+ /generated_perfapi/
53
+ /sdc/
54
+ /traces/
55
+ /build_lib_perfapi/
56
+ /lib_perfapi/
57
+ /systemc_prefix/
58
+ /arm_tlm_prefix/
59
+ dist/
60
+ *.whl
61
+
62
+ # temp directories for python builds
63
+ __pycache__/
64
+ .pytest_cache/
65
+ .hypothesis/
66
+ *.egg-info/
67
+ *.pyc
68
+
69
+ # jupyter notebooks and jupyter lab
70
+ .ipynb_checkpoints/
71
+ .jupyter/
72
+
73
+
74
+ # run area for hw tests
75
+ run_area/
76
+
77
+ # sw generated sv, fl, trace, json, dot, xml, svh, sdc files, zip
78
+ src/sw/**/*.sv
79
+ # ...but hand-written SV test fixtures are tracked
80
+ !src/sw/python/tests/data/**/*.sv
81
+ src/sw/**/*.sdc
82
+ src/sw/**/*.svh
83
+ src/sw/**/*.fl
84
+ src/sw/**/*.xml
85
+ src/sw/**/trace.txt
86
+ src/sw/**/trm/
87
+ src/sw/**/noc_datasheet/
88
+ src/sw/python/**/trace
89
+ src/sw/python/**/meta
90
+ src/sw/python/.hypothesis/
91
+ src/sw/python/designs/SW_simulator_trace.txt
92
+ src/sw/python/jupyter_notebooks/*.json
93
+ src/sw/python/**/generated_rtl/
94
+ src/sw/python/**/generated_perfapi/
95
+ src/sw/python/**/traces/*
96
+ src/sw/python/designs/**/trace/**/*.txt
97
+ src/sw/python/designs/**/meta/*.txt
98
+ src/sw/python/designs/**/meta/*.xml
99
+ src/sw/python/designs/**/meta/*.json
100
+ src/sw/python/designs/tt_ring.json
101
+ src/sw/python/jupyter_notebooks/interconnect_string.txt
102
+ src/sw/python/jupyter_notebooks/mesh_sweep.csv
103
+ routs_src*.csv
104
+
105
+ # randomly generated vps configs
106
+ src/hw/configs/vps_random/**/*.sv
107
+ src/hw/configs/vps_random/**/*.fl
108
+ src/hw/configs/vps_random/**/*.txt
109
+
110
+ ### VisualStudioCode ###
111
+ .vscode/**/* # Maybe .vscode/**/* instead - see comments
112
+ !.vscode/settings.json
113
+ !.vscode/tasks.json
114
+ !.vscode/launch.json
115
+ !.vscode/extensions.json
116
+
117
+ ### MacOS
118
+ .DS_Store
119
+
120
+ ### Generated outputs
121
+ *.numbers
122
+ *.xlsx
123
+ *.log
124
+ *.prof
125
+ sim_output.txt
126
+ *output*
127
+ src/sw/python/jupyter_notebooks/tt_ring.json
128
+ src/sw/python/jupyter_notebooks/**/*.txt
129
+ *.dot
130
+ docs/out/
131
+ api_trace*
132
+ sim_log*.txt
133
+ sim_events.json
134
+ replay_events.json
135
+
136
+ # FabricStudio GUI — generated outputs
137
+ gui/designs/index.json
138
+ gui/designs/*.elab.json
139
+ gui/designs/*.sim_*.json
140
+ gui/output/
141
+
142
+ # Include xls files in the customer directory
143
+ !customer/**/*.xls*
144
+ customer/**/~*.xls*
145
+
146
+ # Ctags for Verilog auti-browse
147
+ tags
148
+
149
+ # Random testcases
150
+ random_test
151
+
152
+
153
+ generate_release/fabric_studio/dependencies/pyarmor-regfile-4186.zip
154
+ release_wheel_temp/
155
+
156
+ # temporary PID file created when building bindings to ensure we don't run multiple builds at once
157
+ build_bindings.pid
158
+
159
+ # temp copy of assets for frontend testing in CacheSim
160
+ src/sw/cachesim_py/Notebooks/assets
161
+ generate_release/cachesim/release_dir/jupyter_notebooks/assets
162
+ generate_release/cachesim/dependencies/pyarmor-regfile-4186.zip
163
+
164
+ # sphinx
165
+ src/sw/python/docs/_build
166
+ src/sw/python/_build
167
+ src/sw/cachesim_py/docs/_build
168
+
169
+ src/sw/python/designs/img
170
+
171
+ cache/
172
+ Arch/oft_home*
173
+ report.html
174
+ report.xml
175
+
176
+ # flopgen accuracy/error reports (diagnostic artifacts of `just rebuild_flops` /
177
+ # `just report_datapoints`)
178
+ flopgen_report*/
179
+
180
+ # ipxact schemas
181
+ src/sw/python/fabric_studio/data_models/sv/ipxact_schemas/
182
+
183
+ # clangd cache
184
+ .cache/clangd
185
+
186
+ # claude superpowers plugin markdowns
187
+ docs/superpowers/
188
+
189
+ # architecture decision records (local only — not for the company repo)
190
+ docs/adr/
191
+
192
+ # domain-context notes (local scratch — durable facts live in CLAUDE.md)
193
+ CONTEXT.md
194
+
195
+ # claude code local settings (per-user, not shared) -- at every level
196
+ **/.claude/settings.json
197
+
198
+ # mempalace (claude code plugin) generated files
199
+ entities.json
200
+ mempalace.yaml
201
+
202
+ # git worktrees (per-user, not shared)
203
+ .claude/worktrees/
204
+ # git worktrees + venvs built by coverage/collect.py for historical FS versions
205
+ /fs-worktrees/
206
+ # worktrees + venvs built by setup_alt_venv.py (just setup-alt-venv <ref>)
207
+ /.alt-venv-worktrees/
208
+ /.venv-*
209
+ # uv cache, only when setup_sw_env.py cannot place it beside the clone
210
+ /.uv-cache/
211
+ # coverage collector --dryrun / baseline_dryrun.sh output (per-user, not shared)
212
+ coverage_dryrun/
213
+
214
+ # local secrets — never commit
215
+ .env
216
+
217
+ # local ruff settings
218
+ .ruff.toml
219
+
220
+ # FabricStudio SaaS per-server config + secrets (never commit)
221
+ gui/saas_poc/setup/saas_config.env
222
+ saas_secrets/
223
+ gui/saas_poc/local/smtp.env
bayafs-0.1.0/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Copyright (c) Baya Systems, Inc. All rights reserved.
2
+
3
+ This package installs and operates FabricStudio, licensed software of Baya Systems.
4
+
5
+ Permission is granted to install and use this package solely to deploy and operate
6
+ FabricStudio under a valid licence agreement with Baya Systems. The FabricStudio software
7
+ this package installs is distributed separately and is governed by that agreement.
8
+
9
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
bayafs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: bayafs
3
+ Version: 0.1.0
4
+ Summary: Install and operate FabricStudio on your own infrastructure.
5
+ Project-URL: Homepage, https://bayasystems.com
6
+ Project-URL: Documentation, https://bayasystems.com/docs/self-hosted
7
+ Author-email: Baya Systems <support@bayasystems.com>
8
+ License-Expression: LicenseRef-Proprietary
9
+ License-File: LICENSE
10
+ Keywords: eda,fabricstudio,installer,noc,self-hosted
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Installation/Setup
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pyyaml>=6
20
+ Requires-Dist: rich>=13.7
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Requires-Dist: ruff; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # baya — FabricStudio, self-hosted
27
+
28
+ Installs and operates [FabricStudio](https://bayasystems.com) on your own Linux server:
29
+ one command brings up the whole platform, and each engineer works in their own workspace,
30
+ on their own home directory, signed in through your identity provider.
31
+
32
+ Published as `bayafs`; the command is `baya`.
33
+
34
+ ## Requirements
35
+
36
+ - Linux x86-64 — RHEL/Oracle, Ubuntu/Debian, or SLES
37
+ - 4+ CPUs, 16 GB+ RAM, 100 GB+ free disk
38
+ - root (the installer manages docker, Kubernetes, and port 443)
39
+ - Outbound HTTPS to Baya's token broker and to Google Artifact Registry
40
+ - A licence key from Baya
41
+
42
+ ## Install
43
+
44
+ ```sh
45
+ pip install bayafs
46
+
47
+ baya doctor # check this host; run it before anything else
48
+ baya license set <your-licence-key>
49
+ baya storage set --base /home # where your engineers' home directories live
50
+ baya init --hostname fs.yourcompany.com
51
+ ```
52
+
53
+ `init` installs what is missing, starts a single-node Kubernetes cluster, pulls the images
54
+ your licence entitles you to, and brings up the platform. It is safe to re-run: a failed
55
+ step is resumed, not repeated.
56
+
57
+ ## Give people access
58
+
59
+ ```sh
60
+ baya user map alice@yourcompany.com --linux-user alice --name "Alice Chen"
61
+ baya verify
62
+ ```
63
+
64
+ `user map` binds a login to a local account and starts that person's workspace on their real
65
+ home directory — files their designs produce are owned by them, exactly as if they had run
66
+ the tools from a shell. `verify` proves the install works end to end and names the command
67
+ to run for anything that does not.
68
+
69
+ ## Day to day
70
+
71
+ ```sh
72
+ baya status # version, health, users, certificate, licence
73
+ baya verify # end-to-end check, after every change
74
+ baya update --check # versions newer than the one installed
75
+ baya update # drains running work first; --force skips the wait
76
+ baya rollback # back to the previous version
77
+ baya workspace list # who is using what
78
+ baya logs <service> # no kubectl required
79
+ baya support-bundle # one tarball for Baya, secrets and designs redacted
80
+ ```
81
+
82
+ ## Certificates and identity
83
+
84
+ A self-signed certificate is generated so day one works; replace it with yours:
85
+
86
+ ```sh
87
+ baya tls set --cert /path/to/fullchain.pem --key /path/to/key.pem
88
+ ```
89
+
90
+ Until an identity provider is federated, people sign in with a username and password held
91
+ by the bundled Keycloak. To use your own:
92
+
93
+ ```sh
94
+ baya idp add --type entra --tenant <id> --client-id <id> --client-secret <secret>
95
+ baya idp test
96
+ ```
97
+
98
+ ## Uninstall
99
+
100
+ ```sh
101
+ baya uninstall --yes
102
+ ```
103
+
104
+ Removes the cluster and the install's own state. **Your engineers' files are never touched.**
105
+
106
+ ---
107
+
108
+ Licensed software. `baya` itself is freely installable; running FabricStudio requires a
109
+ licence key issued by Baya Systems.
bayafs-0.1.0/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # baya — FabricStudio, self-hosted
2
+
3
+ Installs and operates [FabricStudio](https://bayasystems.com) on your own Linux server:
4
+ one command brings up the whole platform, and each engineer works in their own workspace,
5
+ on their own home directory, signed in through your identity provider.
6
+
7
+ Published as `bayafs`; the command is `baya`.
8
+
9
+ ## Requirements
10
+
11
+ - Linux x86-64 — RHEL/Oracle, Ubuntu/Debian, or SLES
12
+ - 4+ CPUs, 16 GB+ RAM, 100 GB+ free disk
13
+ - root (the installer manages docker, Kubernetes, and port 443)
14
+ - Outbound HTTPS to Baya's token broker and to Google Artifact Registry
15
+ - A licence key from Baya
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ pip install bayafs
21
+
22
+ baya doctor # check this host; run it before anything else
23
+ baya license set <your-licence-key>
24
+ baya storage set --base /home # where your engineers' home directories live
25
+ baya init --hostname fs.yourcompany.com
26
+ ```
27
+
28
+ `init` installs what is missing, starts a single-node Kubernetes cluster, pulls the images
29
+ your licence entitles you to, and brings up the platform. It is safe to re-run: a failed
30
+ step is resumed, not repeated.
31
+
32
+ ## Give people access
33
+
34
+ ```sh
35
+ baya user map alice@yourcompany.com --linux-user alice --name "Alice Chen"
36
+ baya verify
37
+ ```
38
+
39
+ `user map` binds a login to a local account and starts that person's workspace on their real
40
+ home directory — files their designs produce are owned by them, exactly as if they had run
41
+ the tools from a shell. `verify` proves the install works end to end and names the command
42
+ to run for anything that does not.
43
+
44
+ ## Day to day
45
+
46
+ ```sh
47
+ baya status # version, health, users, certificate, licence
48
+ baya verify # end-to-end check, after every change
49
+ baya update --check # versions newer than the one installed
50
+ baya update # drains running work first; --force skips the wait
51
+ baya rollback # back to the previous version
52
+ baya workspace list # who is using what
53
+ baya logs <service> # no kubectl required
54
+ baya support-bundle # one tarball for Baya, secrets and designs redacted
55
+ ```
56
+
57
+ ## Certificates and identity
58
+
59
+ A self-signed certificate is generated so day one works; replace it with yours:
60
+
61
+ ```sh
62
+ baya tls set --cert /path/to/fullchain.pem --key /path/to/key.pem
63
+ ```
64
+
65
+ Until an identity provider is federated, people sign in with a username and password held
66
+ by the bundled Keycloak. To use your own:
67
+
68
+ ```sh
69
+ baya idp add --type entra --tenant <id> --client-id <id> --client-secret <secret>
70
+ baya idp test
71
+ ```
72
+
73
+ ## Uninstall
74
+
75
+ ```sh
76
+ baya uninstall --yes
77
+ ```
78
+
79
+ Removes the cluster and the install's own state. **Your engineers' files are never touched.**
80
+
81
+ ---
82
+
83
+ Licensed software. `baya` itself is freely installable; running FabricStudio requires a
84
+ licence key issued by Baya Systems.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bayafs"
7
+ version = "0.1.0"
8
+ description = "Install and operate FabricStudio on your own infrastructure."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-Proprietary"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Baya Systems", email = "support@bayasystems.com" }]
14
+ keywords = ["fabricstudio", "noc", "eda", "self-hosted", "installer"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: System Administrators",
19
+ "Operating System :: POSIX :: Linux",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: System :: Installation/Setup",
22
+ "Topic :: System :: Systems Administration",
23
+ ]
24
+ dependencies = [
25
+ "rich>=13.7",
26
+ # The per-engineer workspace manifest is edited structurally, not textually: its
27
+ # storage must be repointed at that engineer's real home directory.
28
+ "PyYAML>=6",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://bayasystems.com"
33
+ Documentation = "https://bayasystems.com/docs/self-hosted"
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest>=8", "ruff"]
37
+
38
+ [project.scripts]
39
+ baya = "baya.cli:main"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/baya"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ # Tests ship with the sdist so a customer's security team can run them.
46
+ include = ["src", "tests", "README.md", "LICENSE"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ addopts = "-q"
@@ -0,0 +1,11 @@
1
+ """baya — install and operate FabricStudio on your own infrastructure.
2
+
3
+ Distributed as `bayafs` on PyPI; the command is `baya`. Implements FR-B of
4
+ gui/ONPREM_SPEC.md, per gui/ONPREM_BAYA_PLAN.md.
5
+
6
+ Deliberately shares no code with `bayactl` (spec D12/D23): admin tooling must not ship to
7
+ customers. Where the two look alike — output conventions, the subprocess wrapper — the code
8
+ is reimplemented here rather than imported, and the duplication is the point.
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,178 @@
1
+ """Exchanging the license key for a registry credential.
2
+
3
+ The key is a claim ticket: it authenticates the install to Baya, and what comes back is a
4
+ short-lived credential scoped to this customer's own repository. Nothing here can pull an
5
+ image by itself.
6
+
7
+ Credentials last an hour (FR-A7), which is shorter than anything else in an install, so they
8
+ are cached in state and refreshed before expiry rather than fetched per operation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import urllib.error
16
+ import urllib.request
17
+ from dataclasses import dataclass
18
+ from datetime import UTC, datetime, timedelta
19
+
20
+ from . import license as license_mod
21
+ from . import ui
22
+ from .config import Config
23
+ from .errors import BayaError
24
+
25
+ TIMEOUT = 20
26
+ # Refresh this far ahead of expiry: a pull that starts inside the window must not have the
27
+ # credential expire underneath it.
28
+ REFRESH_MARGIN = timedelta(minutes=10)
29
+ FAKE_ENV = "BAYA_FAKE_BROKER"
30
+
31
+
32
+ @dataclass
33
+ class Exchange:
34
+ username: str
35
+ password: str
36
+ expires_at: str
37
+ registry: str
38
+ versions: list[str]
39
+ engines: list[str]
40
+ channel: str
41
+
42
+ @property
43
+ def expiry(self) -> datetime | None:
44
+ try:
45
+ # fromisoformat handles a trailing Z from Python 3.11 on; the broker sends both forms.
46
+ return datetime.fromisoformat(self.expires_at)
47
+ except (ValueError, AttributeError):
48
+ return None
49
+
50
+ def expiring_within(self, margin: timedelta = REFRESH_MARGIN) -> bool:
51
+ expiry = self.expiry
52
+ return expiry is None or expiry - datetime.now(UTC) < margin
53
+
54
+ def to_state(self) -> dict:
55
+ return {
56
+ "credential_username": self.username,
57
+ "credential_password": self.password,
58
+ "credential_expires_at": self.expires_at,
59
+ "registry": self.registry,
60
+ "granted_versions": self.versions,
61
+ "granted_engines": self.engines,
62
+ "channel": self.channel,
63
+ "last_exchange": datetime.now(UTC).isoformat(timespec="seconds"),
64
+ }
65
+
66
+ @classmethod
67
+ def from_state(cls, state: dict) -> Exchange | None:
68
+ if not state.get("credential_password"):
69
+ return None
70
+ return cls(
71
+ username=state.get("credential_username", "oauth2accesstoken"),
72
+ password=state["credential_password"],
73
+ expires_at=state.get("credential_expires_at", ""),
74
+ registry=state.get("registry", ""),
75
+ versions=list(state.get("granted_versions", [])),
76
+ engines=list(state.get("granted_engines", [])),
77
+ channel=state.get("channel", "stable"),
78
+ )
79
+
80
+
81
+ def _fake(key: str) -> Exchange:
82
+ """Development without the broker (plan D24). Never returns a usable credential."""
83
+ return Exchange(
84
+ username="_fake",
85
+ password=f"fake-credential-for-{license_mod.key_id(key)}",
86
+ expires_at=(datetime.now(UTC) + timedelta(hours=1)).isoformat(timespec="seconds"),
87
+ registry="localhost:5000/fs-fake",
88
+ versions=["0.0.1"],
89
+ engines=["latest"],
90
+ channel="stable",
91
+ )
92
+
93
+
94
+ def exchange(
95
+ cfg: Config, key: str, *, version: str | None = None, install_id: str | None = None
96
+ ) -> Exchange:
97
+ """Trade the license key for a credential. Raises BayaError with an actionable message."""
98
+ if not license_mod.is_well_formed(key):
99
+ raise BayaError(f"that license key is not usable: {license_mod.why_invalid(key)}")
100
+ if os.environ.get(FAKE_ENV):
101
+ ui.warn(f"{FAKE_ENV} is set — using a fake credential that cannot pull anything")
102
+ return _fake(key)
103
+
104
+ url = cfg.broker_url.rstrip("/") + "/v1/token"
105
+ payload = {"license_key": key}
106
+ if version:
107
+ payload["version"] = version
108
+ if install_id:
109
+ payload["install_id"] = install_id
110
+ request = urllib.request.Request(
111
+ url,
112
+ data=json.dumps(payload).encode(),
113
+ method="POST",
114
+ headers={"Content-Type": "application/json"},
115
+ )
116
+ try:
117
+ with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
118
+ body = json.loads(response.read())
119
+ except urllib.error.HTTPError as exc:
120
+ raise BayaError(_explain(exc)) from exc
121
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
122
+ raise BayaError(
123
+ f"could not reach {cfg.broker_url}: {getattr(exc, 'reason', exc)}\n"
124
+ " This host needs outbound HTTPS to it; `baya doctor` checks that."
125
+ ) from exc
126
+
127
+ credential = body.get("credential") or {}
128
+ access = body.get("access") or {}
129
+ return Exchange(
130
+ username=credential.get("username", "oauth2accesstoken"),
131
+ password=credential.get("password", ""),
132
+ expires_at=credential.get("expires_at", ""),
133
+ registry=credential.get("registry", ""),
134
+ versions=list(access.get("versions", [])),
135
+ engines=list(access.get("engine_tags", [])),
136
+ channel=access.get("channel", "stable"),
137
+ )
138
+
139
+
140
+ def _explain(exc: urllib.error.HTTPError) -> str:
141
+ """Turn the broker's answer into something an administrator can act on.
142
+
143
+ The broker deliberately says little about why a key was refused; this maps each status
144
+ to the action the administrator would take next.
145
+ """
146
+ try:
147
+ reason = json.loads(exc.read()).get("error", "")
148
+ except (ValueError, OSError):
149
+ reason = ""
150
+ if exc.code == 400:
151
+ return f"the broker rejected the license key: {reason or 'malformed'}"
152
+ if exc.code == 401:
153
+ return (
154
+ "the broker does not recognise this license key.\n"
155
+ " Check it was copied whole, or ask Baya to reissue it."
156
+ )
157
+ if exc.code == 403:
158
+ return (
159
+ f"this license key is not permitted to pull: {reason or 'refused'}\n"
160
+ " Ask Baya to restore access or grant the version you are installing."
161
+ )
162
+ if exc.code >= 500:
163
+ return f"the broker could not issue a credential ({reason or exc.code}); this is Baya's side, retry shortly"
164
+ return f"the broker refused the exchange (HTTP {exc.code}): {reason}"
165
+
166
+
167
+ def current(cfg: Config, *, refresh: bool = False, version: str | None = None) -> Exchange:
168
+ """The credential to use now, exchanging only when the cached one is near expiry."""
169
+ state = cfg.read_state()
170
+ cached = Exchange.from_state(state)
171
+ if cached and not refresh and not cached.expiring_within():
172
+ return cached
173
+ key = state.get("license_key")
174
+ if not key:
175
+ raise BayaError("no license key on this host — run `baya license set <key>` first")
176
+ fresh = exchange(cfg, key, version=version, install_id=state.get("install_id"))
177
+ cfg.update_state(**fresh.to_state())
178
+ return fresh