glovebox-driver 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.
@@ -0,0 +1,180 @@
1
+ .worktrees
2
+ .claude/worktrees/
3
+ .agents/
4
+ .codex/
5
+
6
+ # sbx agent boot trace: a runtime diagnostic the in-VM entrypoint writes into the
7
+ # workspace (read back by bin/checks/sbx/egress.bash); never a committed source file.
8
+ .gb-agent-boot-trace
9
+
10
+ # Dependencies
11
+ # Tool-managed dirs use the BARE name (no trailing slash). A `foo/` pattern
12
+ # matches only a *directory*, so a symlink named `foo` — e.g. one linked into a
13
+ # git worktree to run tests — slips past it and `git add -A` stages it (how a
14
+ # node_modules symlink twice reached CI this repo's history). The bare name
15
+ # matches the dir AND a same-named symlink/file, so tool output can't be
16
+ # committed by accident. Only applied to names that are never a legitimately
17
+ # tracked file (dependency/venv/cache/report dirs); generic names like dist/ or
18
+ # build/ keep the trailing slash so they don't over-ignore a real file.
19
+ node_modules
20
+ .pnpm-store
21
+
22
+ # Build outputs
23
+ dist/
24
+ build/
25
+ out/
26
+
27
+ # Environment files
28
+ .env
29
+ .env.local
30
+ .env.*.local
31
+
32
+ # IDE
33
+ .idea/
34
+ .vscode/
35
+ *.swp
36
+ *.swo
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Logs
43
+ *.log
44
+ npm-debug.log*
45
+ pnpm-debug.log*
46
+
47
+ # Coverage
48
+ coverage/
49
+ coverage.json
50
+ .c8-output
51
+ .coverage
52
+ .coverage.*
53
+ htmlcov
54
+
55
+ # Mutation testing (Stryker): sandbox, HTML report, and the incremental cache
56
+ .stryker-tmp
57
+ reports/
58
+
59
+ # Generated LaunchAgent (rendered from the .template by setup.bash at install time)
60
+ launchagents/*.generated.plist
61
+
62
+ # Python
63
+ __pycache__
64
+ *.pyc
65
+ .venv
66
+ .uv
67
+ .pytest_cache
68
+
69
+ # `uv run` inside inspect-glovebox/ writes a lock beside its pyproject.toml. The wheel is
70
+ # a library, so it pins nothing; committing one adds a `uv.lock -merge` path that
71
+ # scripts/resolve-generated.mjs owns no rule for, which reds the JS suite.
72
+ /inspect-glovebox/uv.lock
73
+ # pytest scratch (repo-relative --basetemp): throwaway fixture files, including
74
+ # fake executables the suite writes (keyprobe, docker stubs). Ignored so they never
75
+ # surface as uncommitted changes or get captured onto a session-end review branch.
76
+ .pytest-tmp
77
+
78
+ # Fuzzer crash reproducers (jazzer.js / atheris drop these in the repo root, which
79
+ # is their cwd). Anchored with a leading slash: an unanchored `crash-*` matches at
80
+ # ANY depth, so it silently swallowed bin/checks/sbx/crash-resilience-int.bash — a
81
+ # `git add` of a legitimately-named source file that reports success and stages
82
+ # nothing.
83
+ /crash-*
84
+ /timeout-*
85
+ /oom-*
86
+ /leak-*
87
+
88
+ # cosmic-ray mutation sessions + HTML reports. mutation-floor.sh writes
89
+ # <session>.html at the repo root for the run's artifact, one per tools/mutation/
90
+ # toml, so this is a glob: a per-session list leaves the next session's report
91
+ # untracked-but-not-ignored, which is what egress-filter and mcpgw-derive were.
92
+ *.sqlite
93
+ /*.html
94
+
95
+ # History working files — each lives on the perf-history or ci-timings branch and is
96
+ # seeded into the checkout by persist-perf-history.sh read or record-setup-timing.sh;
97
+ # never committed to main or a feature branch. A glob, not a list: a tool that walks
98
+ # "tracked plus untracked-not-ignored" takes a seeded file for a repository file, and
99
+ # a per-metric list leaves the next metric's file out of the pattern. That is what
100
+ # widened perf-gates.yaml's generated paths-regex on the runner and nowhere else.
101
+ .github/*-history.json
102
+ .github/*-history.jsonl
103
+
104
+ # pytest --basetemp droppings (used where /tmp is noexec)
105
+ .pytest-exec-tmp
106
+
107
+ # sbx agent-entrypoint boot trace — a runtime log appended into the workspace by
108
+ # gb_boot_trace; a diagnostic artifact, never committed.
109
+ .gb-agent-boot-trace
110
+
111
+ # Hypothesis property-testing example database (regenerated locally; never committed)
112
+ .hypothesis/
113
+
114
+ # CI timing maps that balance the shard fan-outs. They live in R2 (uploaded only by
115
+ # main runs, fetched best-effort by CI — see .github/ci-durations.json) and are never
116
+ # committed; ignored so a CI fetch or a local `uv run pytest --store-durations` /
117
+ # sbx-live run can't accidentally stage them.
118
+ tests/.gb-test-durations.json
119
+ tests/.gb-kcov-durations.json
120
+ tests/.gb-drvfs-durations.json
121
+ tests/.gb-macos-durations.json
122
+ # The plan job's published shard assignment, one per leg (keyed by that leg's
123
+ # duration-map name): derived from the fetched map, shipped to the shards in the
124
+ # same artifact, never committed.
125
+ tests/.gb-*-shard-assignment.json
126
+ .github/sbx-live/durations.json
127
+
128
+ # The pinned scanner binary that .github/scripts/python-deps-vuln-scan.sh downloads
129
+ # into the repo root (~56 MB). Running that script locally to reproduce a red
130
+ # osv-scanner check is the documented response to one, so the artifact lands in
131
+ # every contributor's tree; ignored so it can't be staged by a `git add -A`.
132
+ /osv-scanner
133
+
134
+ # esbuild output of sbx-kit/image/{monitor-dispatch,redact-output}.mjs. The image
135
+ # builds these in its own bundler stage (sbx-kit/image/Dockerfile); the local copies
136
+ # exist so the CT eval harness can subprocess the dispatcher without a docker build.
137
+ sbx-kit/image/monitor-dispatch.bundle.mjs
138
+ sbx-kit/image/redact-output.bundle.mjs
139
+
140
+ # esbuild output of the host guardrail hooks (scripts/build-hook-bundles.mjs), which
141
+ # settings.json launches. Built once per tree by `pnpm install`'s postinstall, by
142
+ # setup.bash at install time, and in the image's own bundler stage — so the reviewable
143
+ # surface is the hook sources plus pnpm-lock.yaml, not a 12k-line generated diff.
144
+ # The hand-written gate-hooks.bundle.d.mts beside them stays tracked.
145
+ .claude/hooks/*.bundle.mjs
146
+
147
+ # The pinned kcov binary that tests/install-kcov-local.sh builds into the repo
148
+ # root — the same path .github/actions/install-kcov caches to, so tests/run-kcov.sh
149
+ # finds a local build with no PATH edit. Reproducing a red "Bash coverage (kcov)"
150
+ # check locally is the documented response to one, so the artifact lands in every
151
+ # contributor's tree; ignored so it can't be staged by a `git add -A`.
152
+ /kcov-bin/
153
+ .claude/.hookpin-*/
154
+
155
+ # synced-deps.test.mjs's untracked-file probe, mkdtemp'd under a synced path on
156
+ # purpose. Ignored so a concurrent `git add -A` elsewhere in this checkout can't
157
+ # stage it mid-test: staged, it would pass trackedUnder()'s check and leave an
158
+ # AD entry in the shared index once the test deletes it from disk.
159
+ .claude/consumer-probe-*/
160
+
161
+ # `ct settings pull` writes this per-checkout pin cache into the repo root. The pin this
162
+ # harness measures lives in evals/control_tower/ct-settings-pin.txt; a committed copy here
163
+ # would be a second one that drifts.
164
+ .settings.local.yml
165
+
166
+ # `gh` writes its per-machine state here when it runs with the repo root as the
167
+ # XDG state home — a device identifier, which no checkout should carry.
168
+ /.local/
169
+
170
+ # Generated doc pages, published to the CDN instead of committed
171
+ # (scripts/render-doc-site.py, .github/workflows/docs-publish.yaml). A committed
172
+ # page is re-derived by every branch that touches its generator's inputs, which
173
+ # made these the busiest files in the history and conflicted branches on bytes
174
+ # nobody typed. Their generators still run, and still refuse a bad input; only
175
+ # the output stays out of the tree.
176
+ /docs/architecture-callgraph.md
177
+ /docs/ci-map.generated.md
178
+ /docs/tla/configs.md
179
+ /docs/tla/reachable-subsets.md
180
+ /docs/tla/diagrams/
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: glovebox-driver
3
+ Version: 0.1.0
4
+ Summary: Host-side Python driver for a glovebox microVM session
5
+ Project-URL: Homepage, https://github.com/AlexanderMattTurner/agent-glovebox
6
+ Project-URL: Source, https://github.com/AlexanderMattTurner/agent-glovebox
7
+ Author: AlexanderMattTurner
8
+ License-Expression: Apache-2.0
9
+ Keywords: ai-control,glovebox,microvm,sandbox
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Security
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: pydantic>=2
16
+ Description-Content-Type: text/markdown
17
+
18
+ # glovebox-driver
19
+
20
+ The host-side Python that drives one [glovebox](https://github.com/AlexanderMattTurner/agent-glovebox) microVM: boot it, run commands in it, read what it let out, and tear it down. It imports no eval framework, so an integration picks the framework and this package holds the VM.
21
+
22
+ `inspect-glovebox` and `shepherd-glovebox` are both built on it.
23
+
24
+ ## What the host needs
25
+
26
+ The driver runs the `glovebox` command, which drives Docker's `sbx` sandbox runtime. Install glovebox and sign in to `sbx`, then ask whether this host qualifies:
27
+
28
+ ```bash
29
+ glovebox sandbox preflight
30
+ ```
31
+
32
+ Set `GLOVEBOX_BIN` when the executable is not on `PATH`.
33
+
34
+ ## The modules
35
+
36
+ | Module | Owns |
37
+ | ------------ | ----------------------------------------------------------------------- |
38
+ | `cli` | resolving the `glovebox` executable and running one `sandbox` verb |
39
+ | `config` | `GloveboxSandboxConfig` — what a VM may be and which hosts it may reach |
40
+ | `session` | `GloveboxSession` — one live microVM, from boot to teardown |
41
+ | `wedge` | booting again when the HOST, not the work, wedged the first attempt |
42
+ | `guest_exec` | the argv for one command inside the guest, as the de-privileged user |
43
+ | `evidence` | reading the policy decision log the sandbox wrote |
44
+ | `leak` | reaping a microVM whose owner died before teardown |
45
+
46
+ ## Boot one
47
+
48
+ ```python
49
+ import os
50
+ from pathlib import Path
51
+
52
+ from glovebox_driver import cli
53
+ from glovebox_driver.session import GloveboxSession
54
+
55
+ workspace = Path("/path/to/workspace")
56
+ workspace.mkdir(parents=True, exist_ok=True)
57
+ os.chmod(workspace, 0o755)
58
+ ready_path = workspace / ".ready"
59
+ ready_path.touch()
60
+
61
+ cli.preflight()
62
+ session = GloveboxSession.boot(str(workspace), str(ready_path), boot_timeout=300)
63
+ try:
64
+ ...
65
+ finally:
66
+ session.teardown()
67
+ ```
68
+
69
+ `boot` waits for `ready_path` to exist; it never creates it. Every in-tree caller
70
+ touches its own marker first, and the guest user needs permission to traverse the
71
+ workspace, so a freshly created directory needs its mode set too.
72
+
73
+ Apache-2.0.
@@ -0,0 +1,56 @@
1
+ # glovebox-driver
2
+
3
+ The host-side Python that drives one [glovebox](https://github.com/AlexanderMattTurner/agent-glovebox) microVM: boot it, run commands in it, read what it let out, and tear it down. It imports no eval framework, so an integration picks the framework and this package holds the VM.
4
+
5
+ `inspect-glovebox` and `shepherd-glovebox` are both built on it.
6
+
7
+ ## What the host needs
8
+
9
+ The driver runs the `glovebox` command, which drives Docker's `sbx` sandbox runtime. Install glovebox and sign in to `sbx`, then ask whether this host qualifies:
10
+
11
+ ```bash
12
+ glovebox sandbox preflight
13
+ ```
14
+
15
+ Set `GLOVEBOX_BIN` when the executable is not on `PATH`.
16
+
17
+ ## The modules
18
+
19
+ | Module | Owns |
20
+ | ------------ | ----------------------------------------------------------------------- |
21
+ | `cli` | resolving the `glovebox` executable and running one `sandbox` verb |
22
+ | `config` | `GloveboxSandboxConfig` — what a VM may be and which hosts it may reach |
23
+ | `session` | `GloveboxSession` — one live microVM, from boot to teardown |
24
+ | `wedge` | booting again when the HOST, not the work, wedged the first attempt |
25
+ | `guest_exec` | the argv for one command inside the guest, as the de-privileged user |
26
+ | `evidence` | reading the policy decision log the sandbox wrote |
27
+ | `leak` | reaping a microVM whose owner died before teardown |
28
+
29
+ ## Boot one
30
+
31
+ ```python
32
+ import os
33
+ from pathlib import Path
34
+
35
+ from glovebox_driver import cli
36
+ from glovebox_driver.session import GloveboxSession
37
+
38
+ workspace = Path("/path/to/workspace")
39
+ workspace.mkdir(parents=True, exist_ok=True)
40
+ os.chmod(workspace, 0o755)
41
+ ready_path = workspace / ".ready"
42
+ ready_path.touch()
43
+
44
+ cli.preflight()
45
+ session = GloveboxSession.boot(str(workspace), str(ready_path), boot_timeout=300)
46
+ try:
47
+ ...
48
+ finally:
49
+ session.teardown()
50
+ ```
51
+
52
+ `boot` waits for `ready_path` to exist; it never creates it. Every in-tree caller
53
+ touches its own marker first, and the guest user needs permission to traverse the
54
+ workspace, so a freshly created directory needs its mode set too.
55
+
56
+ Apache-2.0.
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "glovebox-driver"
3
+ version = "0.1.0"
4
+ description = "Host-side Python driver for a glovebox microVM session"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ authors = [{ name = "AlexanderMattTurner" }]
9
+ keywords = ["glovebox", "sandbox", "microvm", "ai-control"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Science/Research",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Security",
15
+ ]
16
+ # Only pydantic: this package speaks to the `glovebox` CLI over argv and stdio, so it
17
+ # imports no eval framework and pins no version of one.
18
+ dependencies = ["pydantic>=2"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/AlexanderMattTurner/agent-glovebox"
22
+ Source = "https://github.com/AlexanderMattTurner/agent-glovebox"
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/glovebox_driver"]
@@ -0,0 +1,8 @@
1
+ """The host-side lifecycle of one glovebox microVM, with no eval framework attached.
2
+
3
+ `glovebox sandbox` is the supported contract; this package is the Python that drives it.
4
+ Import the module that owns the step you need: `cli` resolves and runs the executable,
5
+ `config` carries what a VM may be and reach, `session` boots one and tears it down,
6
+ `wedge` retries a boot the host wedged, `guest_exec` builds the in-guest exec argv,
7
+ `evidence` reads the policy decision log, and `leak` reaps a VM whose owner died.
8
+ """
@@ -0,0 +1,161 @@
1
+ """Installing the pinned glovebox release when the host has none.
2
+
3
+ PROBLEM CLASS — `pip install` delivers this driver and nothing it drives: the glovebox
4
+ CLI is a bash tree installed by Homebrew, apt or a git clone. When no
5
+ install resolves, this module downloads the release source tarball the wheel pins, refuses
6
+ it unless its SHA-256 matches the pinned digest, and extracts it into a per-user cache —
7
+ so the pip package is the only thing a user consciously installs. An install the user made
8
+ always wins: the caller consults this only after ``$GLOVEBOX_BIN`` and PATH both miss.
9
+ """
10
+
11
+ import hashlib
12
+ import os
13
+ import shutil
14
+ import sys
15
+ import tarfile
16
+ import tempfile
17
+ import time
18
+ import urllib.request
19
+ from pathlib import Path
20
+
21
+ from ._pinned_release import RELEASE_TAG, TARBALL_SHA256
22
+
23
+ _REPO = "AlexanderMattTurner/agent-glovebox"
24
+
25
+ # Bounds on the whole download, not on one socket read. A per-read timeout leaves a server
26
+ # that dribbles a byte inside every window free to block the first sample forever, and a
27
+ # response with no end blocks it just as long while the file grows.
28
+ DOWNLOAD_DEADLINE_S = 600
29
+ DOWNLOAD_MAX_BYTES = 256 * 1024 * 1024
30
+
31
+
32
+ def _tarball_url(tag: str) -> str:
33
+ """The GitHub source tarball for one release tag — the same artifact the AUR and Homebrew packages pin."""
34
+ return f"https://github.com/{_REPO}/archive/refs/tags/{tag}.tar.gz"
35
+
36
+
37
+ def _cache_root() -> Path:
38
+ xdg = os.environ.get("XDG_CACHE_HOME")
39
+ base = Path(xdg) if xdg else Path.home() / ".cache"
40
+ return base / "inspect-glovebox"
41
+
42
+
43
+ class BootstrapFailed(RuntimeError):
44
+ """Raised when the pinned release could not be downloaded, verified, or extracted."""
45
+
46
+
47
+ def ensure_pinned_glovebox() -> str:
48
+ """The cached pinned release's `bin/glovebox`, downloading and verifying it first when absent."""
49
+ return str(
50
+ ensure_glovebox(
51
+ tag=RELEASE_TAG,
52
+ sha256=TARBALL_SHA256,
53
+ url=_tarball_url(RELEASE_TAG),
54
+ cache_root=_cache_root(),
55
+ )
56
+ )
57
+
58
+
59
+ def ensure_glovebox(*, tag: str, sha256: str, url: str, cache_root: Path) -> Path:
60
+ """`bin/glovebox` of the `tag` install under `cache_root`, downloading `url` on a miss.
61
+
62
+ The install lands in a per-tag directory, so a wheel upgrade that moves the pin
63
+ installs beside the old release rather than over it. A concurrent first use is safe:
64
+ each extracts into its own scratch directory and one rename wins.
65
+ """
66
+ wrapper = cache_root / f"glovebox-{tag}" / "bin" / "glovebox"
67
+ if os.access(wrapper, os.X_OK):
68
+ return wrapper
69
+ archive = _fetch_verified(url, sha256)
70
+ try:
71
+ _install(archive, wrapper.parent.parent, cache_root)
72
+ except OSError as exc:
73
+ # An unwritable or full cache is a host state the caller has a remedy for, so it
74
+ # reaches them as the same refusal as a bad download rather than as a traceback.
75
+ raise BootstrapFailed(f"could not install into {cache_root}: {exc}") from exc
76
+ finally:
77
+ archive.unlink(missing_ok=True)
78
+ if not os.access(wrapper, os.X_OK):
79
+ raise BootstrapFailed(
80
+ f"the verified {tag} tarball holds no executable bin/glovebox — "
81
+ f"remove {wrapper.parent.parent} and report this"
82
+ )
83
+ return wrapper
84
+
85
+
86
+ def _fetch_verified(url: str, sha256: str) -> Path:
87
+ """Download `url` to a scratch file and return it only when its digest matches `sha256`.
88
+
89
+ INVARIANT: no byte of the download is extracted or executed before this digest check
90
+ passes — the bootstrap runs code from the network, and the wheel-pinned digest is the
91
+ whole of its authority to do so.
92
+ """
93
+ print(
94
+ f"inspect-glovebox: no glovebox install found — downloading the pinned release "
95
+ f"({url})",
96
+ file=sys.stderr,
97
+ )
98
+ handle, name = tempfile.mkstemp(prefix="glovebox-bootstrap-", suffix=".tar.gz")
99
+ archive = Path(name)
100
+ digest = hashlib.sha256()
101
+ deadline = time.monotonic() + DOWNLOAD_DEADLINE_S
102
+ read = 0
103
+ overran = False
104
+ try:
105
+ # The per-read timeout stops a socket that says nothing; the deadline and the size
106
+ # cap below stop one that says a little forever. TimeoutError is an OSError.
107
+ # read1, not read: read blocks until it has the whole megabyte, so a server
108
+ # dribbling smaller pieces holds one call open past the deadline nobody rechecks.
109
+ with (
110
+ os.fdopen(handle, "wb") as out,
111
+ urllib.request.urlopen(url, timeout=60) as response,
112
+ ): # noqa: S310 # the https URL is module-built from the pinned tag
113
+ while chunk := response.read1(1 << 20):
114
+ read += len(chunk)
115
+ if read > DOWNLOAD_MAX_BYTES or time.monotonic() > deadline:
116
+ overran = True
117
+ break
118
+ digest.update(chunk)
119
+ out.write(chunk)
120
+ except OSError as exc:
121
+ archive.unlink(missing_ok=True)
122
+ raise BootstrapFailed(f"could not download {url}: {exc}") from exc
123
+ if overran:
124
+ archive.unlink(missing_ok=True)
125
+ raise BootstrapFailed(
126
+ f"the download from {url} passed its bound ({DOWNLOAD_MAX_BYTES} bytes or "
127
+ f"{DOWNLOAD_DEADLINE_S}s) at {read} bytes — refusing to keep reading"
128
+ )
129
+ if digest.hexdigest() != sha256:
130
+ archive.unlink(missing_ok=True)
131
+ raise BootstrapFailed(
132
+ f"the download from {url} hashes to {digest.hexdigest()}, not the pinned "
133
+ f"{sha256} — refusing to install unverified bytes"
134
+ )
135
+ return archive
136
+
137
+
138
+ def _install(archive: Path, dest: Path, cache_root: Path) -> None:
139
+ """Extract the verified `archive` into `dest`, atomically enough for concurrent callers."""
140
+ cache_root.mkdir(parents=True, exist_ok=True)
141
+ scratch = Path(tempfile.mkdtemp(prefix=f"{dest.name}.", dir=cache_root))
142
+ try:
143
+ # nosemgrep: tarfile-extractall-traversal — the extractall below passes filter="data", which is the refusal the rule asks for
144
+ with tarfile.open(archive, "r:gz") as tar:
145
+ try:
146
+ # GitHub wraps the tree in one <repo>-<version>/ directory; the data
147
+ # filter is what refuses absolute paths, `..` and out-of-tree links.
148
+ tar.extractall(scratch, filter="data")
149
+ except TypeError as exc:
150
+ raise BootstrapFailed(
151
+ "this Python's tarfile lacks extraction filters (PEP 706; 3.11.4+) — "
152
+ "upgrade Python, or install glovebox yourself"
153
+ ) from exc
154
+ [tree] = scratch.iterdir()
155
+ try:
156
+ tree.rename(dest)
157
+ except OSError:
158
+ if not dest.is_dir():
159
+ raise
160
+ finally:
161
+ shutil.rmtree(scratch, ignore_errors=True)
@@ -0,0 +1,13 @@
1
+ """The glovebox release this wheel bootstraps when no install is found.
2
+
3
+ GENERATED by glovebox-driver/update-pin.sh — edit nothing here by hand. The updater runs
4
+ from sync-packaging.sh on each release, beside the AUR/Homebrew updaters that pin the same
5
+ tarball, and it verifies the download against `git archive` before writing the digest.
6
+ """
7
+
8
+ # The vX.Y.Z release tag whose source tarball the bootstrap downloads.
9
+ RELEASE_TAG = "v0.47.0"
10
+
11
+ # SHA-256 of that tag's GitHub source tarball. The bootstrap refuses bytes that do not
12
+ # hash to this, so the runtime it installs is exactly the one this wheel was cut against.
13
+ TARBALL_SHA256 = "10482b2f8d224bb1b2e74622cc2658765336dccb19bbe8e9a6fc8d39346234ff"