techtree 0.1.0__py3-none-any.whl
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.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The managed Verifiers engine: bundle, installer, registry, runner."""
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""What the engine bundle is, and what it hashes to. Spec section 20.2.
|
|
2
|
+
|
|
3
|
+
The bundle is the static half of the managed engine: a descriptor, a project
|
|
4
|
+
file pinning Verifiers to one commit, the lock that pins everything else, the
|
|
5
|
+
two helper tools that run inside the engine, and the reference package source.
|
|
6
|
+
It contains no Campaign, Climb, DataPolicy, catalog, golden, or schema, which
|
|
7
|
+
is what keeps Campaign generation free of a digest cycle (spec section 20.1).
|
|
8
|
+
|
|
9
|
+
Everything here is content-addressed by the same construction: enumerate the
|
|
10
|
+
files deterministically, describe each one by relative path, size, and content
|
|
11
|
+
digest, sort by path, and digest the canonical JSON manifest. The manifest
|
|
12
|
+
schema is named so that a digest can never be confused with the digest of some
|
|
13
|
+
other kind of tree — an engine bundle and a package tree hash differently even
|
|
14
|
+
if they somehow held identical files.
|
|
15
|
+
|
|
16
|
+
What is excluded from a digest is as much a part of the contract as what is
|
|
17
|
+
included. A virtual environment, a bytecode cache, and the local installation
|
|
18
|
+
marker are all things that appear *because* an engine was installed. Hashing
|
|
19
|
+
them would make an engine's digest change the moment it was used, which is the
|
|
20
|
+
opposite of what a content address is for.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from collections.abc import Iterator, Sequence
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from importlib.resources import files as resource_files
|
|
29
|
+
from importlib.resources.abc import Traversable
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Final
|
|
32
|
+
|
|
33
|
+
from techtree.canonical import digest_object, sha256_digest_bytes
|
|
34
|
+
from techtree.errors import EngineError, ValidationError
|
|
35
|
+
from techtree.fs import ensure_private_directory
|
|
36
|
+
from techtree.models.base import Digest, JsonValue
|
|
37
|
+
from techtree.models.engine import EngineDescriptor
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"BUNDLE_MANIFEST_SCHEMA",
|
|
41
|
+
"DEFAULT_ENGINE_NAME",
|
|
42
|
+
"DESCRIPTOR_FILENAME",
|
|
43
|
+
"ENGINE_TOOLS",
|
|
44
|
+
"INSTALLATION_FILENAME",
|
|
45
|
+
"PACKAGES_DIRECTORY",
|
|
46
|
+
"PACKAGE_MANIFEST_SCHEMA",
|
|
47
|
+
"TOOLS_DIRECTORY",
|
|
48
|
+
"BundleFile",
|
|
49
|
+
"content_manifest",
|
|
50
|
+
"copy_engine_bundle",
|
|
51
|
+
"default_engine_descriptor",
|
|
52
|
+
"default_engine_digest",
|
|
53
|
+
"embedded_engine_root",
|
|
54
|
+
"engine_bundle_digest",
|
|
55
|
+
"enumerate_bundle_files",
|
|
56
|
+
"package_source_digest",
|
|
57
|
+
"read_engine_descriptor",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
DEFAULT_ENGINE_NAME: Final = "default"
|
|
61
|
+
DESCRIPTOR_FILENAME: Final = "engine.json"
|
|
62
|
+
#: Written by the installer when an installation is complete, and therefore
|
|
63
|
+
#: never part of what the bundle hashes to.
|
|
64
|
+
INSTALLATION_FILENAME: Final = "installed.json"
|
|
65
|
+
PACKAGES_DIRECTORY: Final = "packages"
|
|
66
|
+
TOOLS_DIRECTORY: Final = "tools"
|
|
67
|
+
|
|
68
|
+
#: The helpers that run inside the managed engine. Decisions document 0003 A3
|
|
69
|
+
#: puts them inside the digested bundle: they are shipped, hashed, and copied
|
|
70
|
+
#: unchanged, so the code that inspects a taskset is pinned exactly as tightly
|
|
71
|
+
#: as the library it inspects it with.
|
|
72
|
+
ENGINE_TOOLS: Final[tuple[str, ...]] = (
|
|
73
|
+
"inspect_taskset.py",
|
|
74
|
+
"normalize_eval_output.py",
|
|
75
|
+
"normalize_validation.py",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
#: Manifest schema names. They are part of the hashed bytes, so a bundle digest
|
|
79
|
+
#: and a package-tree digest are different values by construction.
|
|
80
|
+
BUNDLE_MANIFEST_SCHEMA: Final = "techtree.engine-bundle.v1"
|
|
81
|
+
#: Decisions document 0003 A8.
|
|
82
|
+
PACKAGE_MANIFEST_SCHEMA: Final = "techtree.package-content.v1"
|
|
83
|
+
|
|
84
|
+
#: Directories that exist because an engine was built or used, never because it
|
|
85
|
+
#: was authored.
|
|
86
|
+
_EXCLUDED_DIRECTORIES: Final[frozenset[str]] = frozenset(
|
|
87
|
+
{".git", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".venv", "__pycache__"}
|
|
88
|
+
)
|
|
89
|
+
#: Files that are local state or operating-system litter rather than content.
|
|
90
|
+
_EXCLUDED_FILENAMES: Final[frozenset[str]] = frozenset(
|
|
91
|
+
{".DS_Store", INSTALLATION_FILENAME}
|
|
92
|
+
)
|
|
93
|
+
_EXCLUDED_SUFFIXES: Final[tuple[str, ...]] = (".pyc", ".pyo", ".tmp")
|
|
94
|
+
|
|
95
|
+
#: An engine name becomes a resource path segment, so it is restricted to a
|
|
96
|
+
#: shape that cannot escape the resources directory.
|
|
97
|
+
_ENGINE_NAME_RE: Final = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class BundleFile:
|
|
102
|
+
"""One file in a hashed tree, described by content rather than by mtime."""
|
|
103
|
+
|
|
104
|
+
relative_path: str
|
|
105
|
+
size: int
|
|
106
|
+
digest: Digest
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def embedded_engine_root(name: str = DEFAULT_ENGINE_NAME) -> Traversable:
|
|
110
|
+
"""Locate one packaged engine bundle inside the installed distribution."""
|
|
111
|
+
if _ENGINE_NAME_RE.fullmatch(name) is None:
|
|
112
|
+
raise ValidationError(
|
|
113
|
+
f"{name!r} is not a valid engine name",
|
|
114
|
+
details={"name": name},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
root = resource_files("techtree") / "resources" / "engines" / name
|
|
118
|
+
if not root.is_dir():
|
|
119
|
+
raise EngineError(
|
|
120
|
+
f"this build does not ship an engine named {name!r}",
|
|
121
|
+
code="engine_bundle_missing",
|
|
122
|
+
details={"name": name},
|
|
123
|
+
)
|
|
124
|
+
return root
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def enumerate_bundle_files(root: Traversable) -> list[BundleFile]:
|
|
128
|
+
"""Enumerate a tree deterministically, by relative path.
|
|
129
|
+
|
|
130
|
+
Sorting is by the POSIX relative path, so the order does not depend on the
|
|
131
|
+
filesystem, the platform, or the order the files were written in.
|
|
132
|
+
"""
|
|
133
|
+
return sorted(_walk(root, prefix=""), key=lambda entry: entry.relative_path)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def content_manifest(
|
|
137
|
+
schema_version: str, files: Sequence[BundleFile]
|
|
138
|
+
) -> dict[str, JsonValue]:
|
|
139
|
+
"""Return the canonical manifest that a tree digest is taken over."""
|
|
140
|
+
return {
|
|
141
|
+
"schema_version": schema_version,
|
|
142
|
+
"files": [
|
|
143
|
+
{
|
|
144
|
+
"path": entry.relative_path,
|
|
145
|
+
"size": entry.size,
|
|
146
|
+
"digest": entry.digest,
|
|
147
|
+
}
|
|
148
|
+
for entry in files
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def engine_bundle_digest(root: Traversable) -> Digest:
|
|
154
|
+
"""Return the digest of one engine bundle's static contents."""
|
|
155
|
+
return digest_object(
|
|
156
|
+
content_manifest(BUNDLE_MANIFEST_SCHEMA, enumerate_bundle_files(root))
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def package_source_digest(package_root: Traversable) -> Digest:
|
|
161
|
+
"""Return the source-tree digest of one shipped package.
|
|
162
|
+
|
|
163
|
+
Decisions document 0003 A8: this is a digest of the *source* the engine
|
|
164
|
+
installs, not of a wheel and not of what lands in site-packages. It is one
|
|
165
|
+
of the three values required to be equal — the Campaign's package digest,
|
|
166
|
+
the engine descriptor's ``source_digest``, and the TasksetLock's
|
|
167
|
+
``resolved_package_digest``.
|
|
168
|
+
"""
|
|
169
|
+
return digest_object(
|
|
170
|
+
content_manifest(PACKAGE_MANIFEST_SCHEMA, enumerate_bundle_files(package_root))
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def read_engine_descriptor(root: Traversable) -> EngineDescriptor:
|
|
175
|
+
"""Load and validate ``engine.json`` from a bundle."""
|
|
176
|
+
descriptor = root / DESCRIPTOR_FILENAME
|
|
177
|
+
if not descriptor.is_file():
|
|
178
|
+
raise EngineError(
|
|
179
|
+
f"engine bundle has no {DESCRIPTOR_FILENAME}",
|
|
180
|
+
code="engine_descriptor_missing",
|
|
181
|
+
details={"file": DESCRIPTOR_FILENAME},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
raw = descriptor.read_bytes()
|
|
185
|
+
try:
|
|
186
|
+
return EngineDescriptor.model_validate_json(raw)
|
|
187
|
+
except ValueError as error:
|
|
188
|
+
raise ValidationError(
|
|
189
|
+
f"engine descriptor is not a valid EngineDescriptor: {error}",
|
|
190
|
+
details={"file": DESCRIPTOR_FILENAME},
|
|
191
|
+
) from error
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def copy_engine_bundle(root: Traversable, destination: Path) -> None:
|
|
195
|
+
"""Copy exactly the hashed files of a bundle into an empty directory.
|
|
196
|
+
|
|
197
|
+
Only enumerated files are written, so an installed bundle contains the same
|
|
198
|
+
bytes the digest was taken over and nothing else — no bytecode cache from
|
|
199
|
+
the developer's machine, no stray editor file. Content is written rather
|
|
200
|
+
than linked: an installed engine must not change because a source tree
|
|
201
|
+
somewhere else did.
|
|
202
|
+
"""
|
|
203
|
+
if destination.is_dir() and any(destination.iterdir()):
|
|
204
|
+
raise EngineError(
|
|
205
|
+
f"refusing to copy an engine bundle into a non-empty directory: "
|
|
206
|
+
f"{destination}",
|
|
207
|
+
code="engine_destination_not_empty",
|
|
208
|
+
details={"path": str(destination)},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
ensure_private_directory(destination)
|
|
212
|
+
for entry in enumerate_bundle_files(root):
|
|
213
|
+
target = destination / entry.relative_path
|
|
214
|
+
ensure_private_directory(target.parent)
|
|
215
|
+
source: Traversable = root
|
|
216
|
+
for segment in entry.relative_path.split("/"):
|
|
217
|
+
source = source / segment
|
|
218
|
+
target.write_bytes(source.read_bytes())
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def default_engine_descriptor() -> EngineDescriptor:
|
|
222
|
+
"""Return the descriptor of the engine this build ships."""
|
|
223
|
+
return read_engine_descriptor(embedded_engine_root())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def default_engine_digest() -> Digest:
|
|
227
|
+
"""Return the digest of the engine bundle this build ships."""
|
|
228
|
+
return engine_bundle_digest(embedded_engine_root())
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _walk(root: Traversable, *, prefix: str) -> Iterator[BundleFile]:
|
|
232
|
+
"""Yield every included file under ``root``, depth first."""
|
|
233
|
+
for entry in root.iterdir():
|
|
234
|
+
name = entry.name
|
|
235
|
+
relative = f"{prefix}{name}"
|
|
236
|
+
|
|
237
|
+
if entry.is_dir():
|
|
238
|
+
if name in _EXCLUDED_DIRECTORIES:
|
|
239
|
+
continue
|
|
240
|
+
yield from _walk(entry, prefix=f"{relative}/")
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
if name in _EXCLUDED_FILENAMES or name.endswith(_EXCLUDED_SUFFIXES):
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
content = entry.read_bytes()
|
|
247
|
+
yield BundleFile(
|
|
248
|
+
relative_path=relative,
|
|
249
|
+
size=len(content),
|
|
250
|
+
digest=sha256_digest_bytes(content),
|
|
251
|
+
)
|