toolyard 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 (60) hide show
  1. toolyard-0.1.0/.editorconfig +23 -0
  2. toolyard-0.1.0/.github/workflows/ci.yml +136 -0
  3. toolyard-0.1.0/.github/workflows/release.yml +57 -0
  4. toolyard-0.1.0/.gitignore +104 -0
  5. toolyard-0.1.0/.importlinter +132 -0
  6. toolyard-0.1.0/.pre-commit-config.yaml +22 -0
  7. toolyard-0.1.0/CHANGELOG.md +250 -0
  8. toolyard-0.1.0/CONTRIBUTING.md +122 -0
  9. toolyard-0.1.0/LICENSE +201 -0
  10. toolyard-0.1.0/PKG-INFO +312 -0
  11. toolyard-0.1.0/README.md +280 -0
  12. toolyard-0.1.0/SECURITY.md +93 -0
  13. toolyard-0.1.0/acceptance/register_and_execute.py +174 -0
  14. toolyard-0.1.0/docs/packages/toolyard/development-plan.md +145 -0
  15. toolyard-0.1.0/docs/packages/toolyard/spec.md +492 -0
  16. toolyard-0.1.0/pyproject.toml +114 -0
  17. toolyard-0.1.0/requirements/README.md +71 -0
  18. toolyard-0.1.0/requirements/ci.lock +778 -0
  19. toolyard-0.1.0/requirements/release.in +6 -0
  20. toolyard-0.1.0/requirements/release.lock +493 -0
  21. toolyard-0.1.0/src/toolyard/__about__.py +5 -0
  22. toolyard-0.1.0/src/toolyard/__init__.py +165 -0
  23. toolyard-0.1.0/src/toolyard/_safe.py +236 -0
  24. toolyard-0.1.0/src/toolyard/containment.py +464 -0
  25. toolyard-0.1.0/src/toolyard/errors.py +122 -0
  26. toolyard-0.1.0/src/toolyard/executor.py +710 -0
  27. toolyard-0.1.0/src/toolyard/py.typed +0 -0
  28. toolyard-0.1.0/src/toolyard/registry.py +165 -0
  29. toolyard-0.1.0/src/toolyard/sandbox.py +1114 -0
  30. toolyard-0.1.0/src/toolyard/store.py +68 -0
  31. toolyard-0.1.0/src/toolyard/tools/__init__.py +54 -0
  32. toolyard-0.1.0/src/toolyard/tools/command.py +230 -0
  33. toolyard-0.1.0/src/toolyard/tools/fetch.py +563 -0
  34. toolyard-0.1.0/src/toolyard/tools/files.py +548 -0
  35. toolyard-0.1.0/src/toolyard/types.py +799 -0
  36. toolyard-0.1.0/src/toolyard/validation.py +389 -0
  37. toolyard-0.1.0/tests/conftest.py +66 -0
  38. toolyard-0.1.0/tests/fakes.py +377 -0
  39. toolyard-0.1.0/tests/fixtures/fetch/adr0026_vectors.json +370 -0
  40. toolyard-0.1.0/tests/goldens/builtin_wire_definitions.json +121 -0
  41. toolyard-0.1.0/tests/goldens/wire_definitions.json +52 -0
  42. toolyard-0.1.0/tests/integration/test_isolation.py +421 -0
  43. toolyard-0.1.0/tests/oracles.py +45 -0
  44. toolyard-0.1.0/tests/performance/test_dispatch_budget.py +118 -0
  45. toolyard-0.1.0/tests/property/test_executor_property.py +261 -0
  46. toolyard-0.1.0/tests/strategies.py +515 -0
  47. toolyard-0.1.0/tests/unit/test_boundaries.py +402 -0
  48. toolyard-0.1.0/tests/unit/test_builtin_wire_definitions.py +101 -0
  49. toolyard-0.1.0/tests/unit/test_command_tool.py +306 -0
  50. toolyard-0.1.0/tests/unit/test_containment.py +387 -0
  51. toolyard-0.1.0/tests/unit/test_executor.py +692 -0
  52. toolyard-0.1.0/tests/unit/test_fetch_tool.py +526 -0
  53. toolyard-0.1.0/tests/unit/test_files_tools.py +581 -0
  54. toolyard-0.1.0/tests/unit/test_records.py +376 -0
  55. toolyard-0.1.0/tests/unit/test_registry.py +154 -0
  56. toolyard-0.1.0/tests/unit/test_safe.py +223 -0
  57. toolyard-0.1.0/tests/unit/test_sandbox.py +1138 -0
  58. toolyard-0.1.0/tests/unit/test_store.py +123 -0
  59. toolyard-0.1.0/tests/unit/test_types.py +429 -0
  60. toolyard-0.1.0/tests/unit/test_validation.py +272 -0
@@ -0,0 +1,23 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+ indent_size = 4
10
+
11
+ [*.py]
12
+ indent_size = 4
13
+ max_line_length = 100
14
+
15
+ [*.{toml,yml,yaml,json}]
16
+ indent_size = 2
17
+
18
+ [*.md]
19
+ trim_trailing_whitespace = false
20
+ max_line_length = off
21
+
22
+ [Makefile]
23
+ indent_style = tab
@@ -0,0 +1,136 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ format:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with: { python-version: "3.12" }
15
+ - run: pip install ruff
16
+ - run: ruff format --check .
17
+
18
+ lint:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with: { python-version: "3.12" }
24
+ - run: pip install ruff
25
+ - run: ruff check .
26
+
27
+ types:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with: { python-version: "3.12" }
33
+ - run: pip install --require-hashes -r requirements/ci.lock
34
+ - run: pip install . --no-deps
35
+ - run: mypy src tests
36
+
37
+ boundaries:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with: { python-version: "3.12" }
43
+ - run: pip install --require-hashes -r requirements/ci.lock
44
+ - run: pip install . --no-deps
45
+ - run: lint-imports
46
+
47
+ tests:
48
+ runs-on: ubuntu-latest
49
+ strategy:
50
+ matrix:
51
+ python-version: ["3.12", "3.13"]
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: actions/setup-python@v5
55
+ with: { python-version: "${{ matrix.python-version }}" }
56
+ - run: pip install --require-hashes -r requirements/ci.lock
57
+ - run: pip install . --no-deps
58
+ - run: pytest -m "not live and not performance" --cov --cov-report=xml
59
+
60
+ tests-314-early-warning:
61
+ runs-on: ubuntu-latest
62
+ continue-on-error: true
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - uses: actions/setup-python@v5
66
+ with: { python-version: "3.14" }
67
+ # Deliberately unpinned: ci.lock is resolved on 3.13, and pinning a version that has no
68
+ # 3.14 wheels would defeat the purpose of an early warning.
69
+ - run: pip install -e ".[dev]"
70
+ - run: pytest -m "not live and not performance"
71
+
72
+ coverage:
73
+ needs: [tests]
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ - uses: actions/setup-python@v5
78
+ with: { python-version: "3.12" }
79
+ - run: pip install --require-hashes -r requirements/ci.lock
80
+ - run: pip install . --no-deps
81
+ - run: pytest -m "not live and not performance" --cov --cov-report=term-missing --cov-fail-under=95
82
+
83
+ contracts:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+ - uses: actions/setup-python@v5
88
+ with: { python-version: "3.12" }
89
+ - run: pip install --require-hashes -r requirements/ci.lock
90
+ - run: pip install . --no-deps
91
+ - run: pytest -m contract
92
+
93
+ security:
94
+ runs-on: ubuntu-latest
95
+ steps:
96
+ - uses: actions/checkout@v4
97
+ # gitleaks scans *history*, and `actions/checkout` fetches a single commit by default.
98
+ # For a push it is handed `<first-pushed>^..<last-pushed>`, so the parent of the first
99
+ # pushed commit has to be in the object store; in a depth-1 clone it is not, and git
100
+ # answers "unknown revision", which the action reports as exit code 1. It fails the same
101
+ # way whether or not a secret exists, so a green run would not have meant anything either.
102
+ with: { fetch-depth: 0 }
103
+ - uses: actions/setup-python@v5
104
+ with: { python-version: "3.12" }
105
+ - run: pip install pip-audit
106
+ # Audit the locked sets, not the job's own environment: a bare `pip-audit` here would
107
+ # inspect an environment containing only pip-audit itself (Security Standards §11).
108
+ - run: pip-audit --require-hashes -r requirements/ci.lock
109
+ - run: pip-audit --require-hashes -r requirements/release.lock
110
+ - uses: gitleaks/gitleaks-action@v2
111
+ env:
112
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113
+
114
+ build:
115
+ runs-on: ubuntu-latest
116
+ steps:
117
+ - uses: actions/checkout@v4
118
+ - uses: actions/setup-python@v5
119
+ with: { python-version: "3.12" }
120
+ - run: pip install --require-hashes -r requirements/release.lock
121
+ - run: python -m build --no-isolation
122
+ - run: twine check dist/*
123
+ - uses: actions/upload-artifact@v4
124
+ with: { name: dist, path: dist/ }
125
+
126
+ install-check:
127
+ needs: [build]
128
+ runs-on: ubuntu-latest
129
+ steps:
130
+ - uses: actions/checkout@v4
131
+ - uses: actions/setup-python@v5
132
+ with: { python-version: "3.12" }
133
+ - uses: actions/download-artifact@v4
134
+ with: { name: dist, path: dist/ }
135
+ - run: pip install dist/*.whl
136
+ - run: python -c "import toolyard"
@@ -0,0 +1,57 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*.*.*"]
6
+ workflow_dispatch: # manual TestPyPI dry run; see publish-testpypi below
7
+
8
+ permissions:
9
+ id-token: write # required for PyPI Trusted Publishing
10
+ contents: write # required to create the GitHub release
11
+
12
+ jobs:
13
+ release:
14
+ # Tag pushes only. Without this, clicking "Run workflow" for the TestPyPI dry run below would
15
+ # also fire this job and publish to real PyPI — the exact opposite of a dry run.
16
+ if: github.event_name == 'push'
17
+ runs-on: ubuntu-latest
18
+ environment: pypi # must match the Environment name set on the PyPI trusted publisher
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with: { python-version: "3.12" }
23
+ # Byte-for-byte the dry run's build chain below. A real release that resolved `build` and
24
+ # `hatchling` fresh from PyPI would not be the artifact the dry run proved.
25
+ - run: pip install --require-hashes -r requirements/release.lock
26
+ - run: python -m build --no-isolation
27
+ - run: twine check dist/*
28
+ - run: pip install "dist/$(ls dist | grep .whl)[dev]"
29
+ - run: pytest -m "not live and not performance"
30
+ - name: Publish to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
32
+ - name: Create GitHub release
33
+ uses: softprops/action-gh-release@v2
34
+ with:
35
+ generate_release_notes: true
36
+ files: dist/*
37
+
38
+ publish-testpypi:
39
+ # Manual only, via Actions -> Release -> Run workflow. Packaging and Release Standards §6
40
+ # requires a successful TestPyPI publish ahead of a package's first real release; 0.1.0 is
41
+ # this package's first published version, so run this once before tagging v0.1.0. Later
42
+ # releases may skip it, or use it again as a dry run.
43
+ if: github.event_name == 'workflow_dispatch'
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - uses: actions/setup-python@v5
48
+ with: { python-version: "3.12" }
49
+ - run: pip install --require-hashes -r requirements/release.lock
50
+ - run: python -m build --no-isolation
51
+ - run: twine check dist/*
52
+ - run: pip install "dist/$(ls dist | grep .whl)[dev]"
53
+ - run: pytest -m "not live and not performance"
54
+ - name: Publish to TestPyPI
55
+ uses: pypa/gh-action-pypi-publish@release/v1
56
+ with:
57
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,104 @@
1
+ # ---- Python ----
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # ---- Packaging / build backends ----
25
+ pip-wheel-metadata/
26
+ share/python-wheels/
27
+
28
+ # ---- Testing / coverage ----
29
+ .pytest_cache/
30
+ .cache
31
+ .coverage
32
+ .coverage.*
33
+ coverage.xml
34
+ *.cover
35
+ *.py,cover
36
+ htmlcov/
37
+ nosetests.xml
38
+ .hypothesis/
39
+
40
+ # ---- Type checking / linting ----
41
+ .mypy_cache/
42
+ .dmypy.json
43
+ dmypy.json
44
+ .ruff_cache/
45
+ .pytype/
46
+
47
+ # ---- Virtual environments ----
48
+ .venv/
49
+ venv/
50
+ ENV/
51
+ env/
52
+ env.bak/
53
+ venv.bak/
54
+ .python-version
55
+
56
+ # ---- Distribution / dependency locking ----
57
+ # requirements/*.lock is deliberately NOT ignored. The lock files are committed
58
+ # inputs: CI and release jobs install from them with --require-hashes (Packaging
59
+ # and Release Standards §4, Security Standards §11), so an ignored lock file
60
+ # means a checkout that cannot build.
61
+
62
+ # ---- Editors / OS ----
63
+ .vscode/
64
+ .idea/
65
+ *.swp
66
+ *.swo
67
+ .DS_Store
68
+ Thumbs.db
69
+
70
+ # ---- Suite runtime state (this component's local data) ----
71
+ # The application writes to XDG paths at runtime (~/.config, ~/.local/share,
72
+ # ~/.local/state per Master Architecture §1.2), never inside the repository.
73
+ # These entries only guard against a developer pointing XDG_* at the repo
74
+ # during local testing.
75
+ .local/
76
+ .config/
77
+ *.sqlite3
78
+ *.sqlite3-journal
79
+ *.sqlite3-wal
80
+ *.sqlite3-shm
81
+ /data/
82
+ /logs/
83
+ /backups/
84
+ /artifacts/
85
+ /exports/
86
+
87
+ # ---- Secrets ----
88
+ # Per Security Standards §8: secrets are never committed. Config files may
89
+ # only name where a secret comes from (*_env / *_file), never the value.
90
+ .env
91
+ .env.*
92
+ *.key
93
+ *.pem
94
+ secrets.toml
95
+
96
+ # ---- Node-free JS assets (MirrorWall consumers may still use a local tool) ----
97
+ node_modules/
98
+
99
+ # ---- Build artifacts from docs generation ----
100
+ docs/api/openapi-v1.json.tmp
101
+
102
+ # ---- import-linter cache ----
103
+ # `lint-imports` writes this on every gate run; it is a cache, never an input.
104
+ .import_linter_cache/
@@ -0,0 +1,132 @@
1
+ [importlinter]
2
+ root_package = toolyard
3
+ include_external_packages = True
4
+
5
+ [importlinter:contract:no-application-imports]
6
+ # Master architecture §2: packages never import applications. Never weakened, never extended —
7
+ # a package that "needs" an application type has been handed application responsibility.
8
+ name = ToolYard must not import applications
9
+ type = forbidden
10
+ source_modules = toolyard
11
+ forbidden_modules =
12
+ freeweight
13
+ loadcoach
14
+ ideapress
15
+ promptcadence
16
+
17
+ [importlinter:contract:no-sibling-packages]
18
+ # Spec §5: "Nothing else — in particular not `setspec` (records are in-process values;
19
+ # applications serialize them) and no sibling capability package." Never weakened, never extended.
20
+ name = ToolYard must not import sibling capability packages
21
+ type = forbidden
22
+ source_modules = toolyard
23
+ forbidden_modules =
24
+ setspec
25
+ modelrack
26
+ sweatmeter
27
+ weightsdb
28
+ mirrorwall
29
+ cutctx
30
+ loadledger
31
+ commissioner
32
+ spotcheck
33
+
34
+ [importlinter:contract:no-model-access]
35
+ # Spec §3: "No model access and no provider JSON." ToolYard exports a neutral wire definition and
36
+ # the caller adapts it; there is no client here to repurpose. Never weakened, never extended.
37
+ #
38
+ # allow_indirect_imports: these contracts describe what *this package's own modules* import. A
39
+ # chain through a dependency would be that dependency's budget to answer for.
40
+ name = ToolYard must hold no path to a model
41
+ allow_indirect_imports = True
42
+ type = forbidden
43
+ source_modules = toolyard
44
+ forbidden_modules =
45
+ openai
46
+ anthropic
47
+ ollama
48
+ litellm
49
+ transformers
50
+ torch
51
+ huggingface_hub
52
+
53
+ [importlinter:contract:no-persistence]
54
+ # Spec §10: "Data ownership: none." The application persists records through its own
55
+ # `ToolCallStore`; ToolYard defines the protocol and an in-memory implementation for tests.
56
+ # Never weakened, never extended.
57
+ name = ToolYard must touch no database
58
+ allow_indirect_imports = True
59
+ type = forbidden
60
+ source_modules = toolyard
61
+ forbidden_modules =
62
+ sqlalchemy
63
+ alembic
64
+ sqlite3
65
+
66
+ [importlinter:contract:subprocess-only-in-the-sandbox]
67
+ # LAYERED, and written for the module that does not exist yet. ADR-0053 decision 5 and ADR-0018
68
+ # put every process launch behind one door: the container → bwrap → refuse ladder. Phase 2 (row D1)
69
+ # writes `toolyard.sandbox` and *that module alone* may import `subprocess`; the exemption below is
70
+ # already here so D1 adds a module rather than editing a boundary rule.
71
+ #
72
+ # `unmatched_ignore_imports_alerting = none` is what lets an exemption name an import that does not
73
+ # exist yet. It is not a weakening: any *other* module importing `subprocess` still fails.
74
+ name = Only the sandbox may import subprocess
75
+ allow_indirect_imports = True
76
+ unmatched_ignore_imports_alerting = none
77
+ type = forbidden
78
+ source_modules = toolyard
79
+ forbidden_modules =
80
+ subprocess
81
+ multiprocessing
82
+ pty
83
+ ignore_imports =
84
+ toolyard.sandbox -> subprocess
85
+
86
+ [importlinter:contract:http-only-in-the-fetch-tool]
87
+ # LAYERED, same shape. ADR-0053 decision 6 puts the ADR-0026 §3 checks where the socket is opened.
88
+ # Phase 3 (row E2) writes `toolyard.tools.fetch` and *that module alone* may import `httpx`; every
89
+ # other HTTP client stays forbidden outright, in every module, forever — a second client is a
90
+ # second chance to omit the redirect check.
91
+ name = Only the fetch tool may hold an HTTP client
92
+ allow_indirect_imports = True
93
+ unmatched_ignore_imports_alerting = none
94
+ type = forbidden
95
+ source_modules = toolyard
96
+ forbidden_modules =
97
+ httpx
98
+ requests
99
+ aiohttp
100
+ urllib3
101
+ urllib
102
+ http
103
+ socket
104
+ ssl
105
+ ignore_imports =
106
+ toolyard.tools.fetch -> httpx
107
+
108
+ [importlinter:contract:filesystem-only-where-containment-lives]
109
+ # LAYERED. Spec §14: no argument is ever interpolated into a path. The way that is kept true is
110
+ # that path handling exists in one place — `toolyard.containment` — which resolves before it
111
+ # compares. Phase 2's `toolyard.sandbox` and Phase 3's `toolyard.tools.files` are the two modules
112
+ # that join it; their exemptions are already written.
113
+ name = Filesystem access lives only where containment does
114
+ allow_indirect_imports = True
115
+ unmatched_ignore_imports_alerting = none
116
+ type = forbidden
117
+ source_modules = toolyard
118
+ forbidden_modules =
119
+ os
120
+ pathlib
121
+ shutil
122
+ tempfile
123
+ glob
124
+ fnmatch
125
+ ignore_imports =
126
+ toolyard.containment -> pathlib
127
+ toolyard.containment -> os
128
+ toolyard.sandbox -> pathlib
129
+ toolyard.sandbox -> os
130
+ toolyard.sandbox -> shutil
131
+ toolyard.sandbox -> tempfile
132
+ toolyard.tools.files -> pathlib
@@ -0,0 +1,22 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.6.9
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/pre-commit-hooks
9
+ rev: v4.6.0
10
+ hooks:
11
+ - id: trailing-whitespace
12
+ - id: end-of-file-fixer
13
+ - id: check-toml
14
+ - id: check-json
15
+ - id: check-added-large-files
16
+ - id: check-merge-conflict
17
+ - id: mixed-line-ending
18
+ args: [--fix=lf]
19
+ - repo: https://github.com/gitleaks/gitleaks
20
+ rev: v8.18.4
21
+ hooks:
22
+ - id: gitleaks