paxlet 0.1.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.
paxlet-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: paxlet
3
+ Version: 0.1.1
4
+ Summary: Small, addressable packages of resources and actions for connected systems.
5
+ Author: Paxlet contributors
6
+ Author-email: Tom Sapletta <tom@sapletta.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+
11
+ # paxlet
12
+
13
+
14
+ ## AI Cost Tracking
15
+
16
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.1-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-1.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
18
+
19
+ - 🤖 **LLM usage:** $0.0466 (1 commits)
20
+ - 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
21
+
22
+ Generated on 2026-09-22 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
23
+
24
+ ---
25
+
26
+
27
+
28
+ **Build small. Connect everything.**
29
+
30
+ Paxlet Core is a deliberately small contract for packaging **resources and actions** as addressable capabilities.
31
+
32
+ A Paxlet has a stable identity, a version, clear inputs/outputs, explicit dependencies and requested permissions. A node can resolve it, verify it, run it and produce a receipt.
33
+
34
+ ```text
35
+ URN identity
36
+
37
+
38
+ ┌──────────── Paxlet ────────────┐
39
+ │ resources │
40
+ │ actions │
41
+ │ dependencies │
42
+ │ permissions │
43
+ │ provenance │
44
+ └───────────────────────────────┘
45
+
46
+
47
+ URI locations → Node → Receipt
48
+ ```
49
+
50
+ ## Start in five minutes
51
+
52
+ Requires Python 3.11+ and no third-party runtime dependencies. You can run it directly after cloning:
53
+
54
+ ```bash
55
+ ./bin/paxlet init /tmp/my-first
56
+ ./bin/paxlet run /tmp/my-first hello --input '{"name":"Ada"}'
57
+
58
+ ./bin/paxlet verify examples/hello
59
+ ./bin/paxlet run examples/hello hello --input '{"name":"Ada"}'
60
+ ```
61
+
62
+ Optional developer install:
63
+
64
+ ```bash
65
+ python -m venv .venv
66
+ . .venv/bin/activate
67
+ pip install -e .
68
+ ```
69
+
70
+ Or address the same capability by identity:
71
+
72
+ ```bash
73
+ ./bin/paxlet resolve urn:paxlet:example:hello
74
+ ./bin/paxlet run urn:paxlet:example:hello hello --input '{"name":"Ada"}'
75
+ ```
76
+
77
+ Try a science-oriented example:
78
+
79
+ ```bash
80
+ ./bin/paxlet run examples/science-fasta-gc analyze --input '{}'
81
+ ./bin/paxlet run examples/statistics-mean mean --input '{"values":[1,2,4,8]}'
82
+ ```
83
+
84
+ ## CLI
85
+
86
+ ```text
87
+ paxlet init DIRECTORY
88
+ paxlet verify PATH|URN
89
+ paxlet inspect PATH|URN
90
+ paxlet resolve URN
91
+ paxlet run PATH|URN ACTION --input JSON
92
+ paxlet pack PATH|URN -o package.paxlet.zip
93
+ ```
94
+
95
+ ## Why Core is small
96
+
97
+ Paxlet is not an AI-agent framework. It is the portable unit that larger systems can use.
98
+
99
+ ```text
100
+ Paxlet Core package + identity + contract
101
+ Paxlet Node resolution + grants + execution + receipts
102
+ Paxlet Network registries + peers + distribution
103
+ Taskand planning + orchestration + adaptive-system showcase
104
+ ```
105
+
106
+ Taskand can demonstrate planners, processes, federation, mesh, Digital Twin, MCP, governance and recovery without making those features mandatory for every Paxlet implementation.
107
+
108
+ ## Repository map
109
+
110
+ ```text
111
+ SPEC.md normative Core 0.1 draft
112
+ schemas/ manifest + receipt schemas
113
+ paxlet/ tiny Python reference runtime
114
+ examples/ five understandable capabilities
115
+ registry/local.json local URN → URI resolver example
116
+ conformance/ implementation compatibility runner
117
+ docs/ addressing, security, receipts, layers
118
+ integrations/taskand/ how Taskand fits as the showcase
119
+ ```
120
+
121
+ ## Conformance
122
+
123
+ ```bash
124
+ python -m unittest discover -s tests -v
125
+ python conformance/run.py
126
+ ```
127
+
128
+ ## Important security note
129
+
130
+ The reference runtime is intentionally educational. It validates package paths and exposes secrets only after explicit user grant, but it **does not enforce filesystem/network permissions with an OS sandbox**. Production nodes should use containers, VMs or OS policy for enforcement. See `docs/security.md`.
131
+
132
+ ## Status
133
+
134
+ Core 0.1 is a draft intended to make the idea testable and implementable by independent runtimes. See `ROADMAP.md`.
135
+
136
+
137
+ ## License
138
+
139
+ Licensed under Apache-2.0.
paxlet-0.1.1/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # paxlet
2
+
3
+
4
+ ## AI Cost Tracking
5
+
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.1-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-1.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
+
9
+ - 🤖 **LLM usage:** $0.0466 (1 commits)
10
+ - 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
11
+
12
+ Generated on 2026-09-22 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
+
14
+ ---
15
+
16
+
17
+
18
+ **Build small. Connect everything.**
19
+
20
+ Paxlet Core is a deliberately small contract for packaging **resources and actions** as addressable capabilities.
21
+
22
+ A Paxlet has a stable identity, a version, clear inputs/outputs, explicit dependencies and requested permissions. A node can resolve it, verify it, run it and produce a receipt.
23
+
24
+ ```text
25
+ URN identity
26
+
27
+
28
+ ┌──────────── Paxlet ────────────┐
29
+ │ resources │
30
+ │ actions │
31
+ │ dependencies │
32
+ │ permissions │
33
+ │ provenance │
34
+ └───────────────────────────────┘
35
+
36
+
37
+ URI locations → Node → Receipt
38
+ ```
39
+
40
+ ## Start in five minutes
41
+
42
+ Requires Python 3.11+ and no third-party runtime dependencies. You can run it directly after cloning:
43
+
44
+ ```bash
45
+ ./bin/paxlet init /tmp/my-first
46
+ ./bin/paxlet run /tmp/my-first hello --input '{"name":"Ada"}'
47
+
48
+ ./bin/paxlet verify examples/hello
49
+ ./bin/paxlet run examples/hello hello --input '{"name":"Ada"}'
50
+ ```
51
+
52
+ Optional developer install:
53
+
54
+ ```bash
55
+ python -m venv .venv
56
+ . .venv/bin/activate
57
+ pip install -e .
58
+ ```
59
+
60
+ Or address the same capability by identity:
61
+
62
+ ```bash
63
+ ./bin/paxlet resolve urn:paxlet:example:hello
64
+ ./bin/paxlet run urn:paxlet:example:hello hello --input '{"name":"Ada"}'
65
+ ```
66
+
67
+ Try a science-oriented example:
68
+
69
+ ```bash
70
+ ./bin/paxlet run examples/science-fasta-gc analyze --input '{}'
71
+ ./bin/paxlet run examples/statistics-mean mean --input '{"values":[1,2,4,8]}'
72
+ ```
73
+
74
+ ## CLI
75
+
76
+ ```text
77
+ paxlet init DIRECTORY
78
+ paxlet verify PATH|URN
79
+ paxlet inspect PATH|URN
80
+ paxlet resolve URN
81
+ paxlet run PATH|URN ACTION --input JSON
82
+ paxlet pack PATH|URN -o package.paxlet.zip
83
+ ```
84
+
85
+ ## Why Core is small
86
+
87
+ Paxlet is not an AI-agent framework. It is the portable unit that larger systems can use.
88
+
89
+ ```text
90
+ Paxlet Core package + identity + contract
91
+ Paxlet Node resolution + grants + execution + receipts
92
+ Paxlet Network registries + peers + distribution
93
+ Taskand planning + orchestration + adaptive-system showcase
94
+ ```
95
+
96
+ Taskand can demonstrate planners, processes, federation, mesh, Digital Twin, MCP, governance and recovery without making those features mandatory for every Paxlet implementation.
97
+
98
+ ## Repository map
99
+
100
+ ```text
101
+ SPEC.md normative Core 0.1 draft
102
+ schemas/ manifest + receipt schemas
103
+ paxlet/ tiny Python reference runtime
104
+ examples/ five understandable capabilities
105
+ registry/local.json local URN → URI resolver example
106
+ conformance/ implementation compatibility runner
107
+ docs/ addressing, security, receipts, layers
108
+ integrations/taskand/ how Taskand fits as the showcase
109
+ ```
110
+
111
+ ## Conformance
112
+
113
+ ```bash
114
+ python -m unittest discover -s tests -v
115
+ python conformance/run.py
116
+ ```
117
+
118
+ ## Important security note
119
+
120
+ The reference runtime is intentionally educational. It validates package paths and exposes secrets only after explicit user grant, but it **does not enforce filesystem/network permissions with an OS sandbox**. Production nodes should use containers, VMs or OS policy for enforcement. See `docs/security.md`.
121
+
122
+ ## Status
123
+
124
+ Core 0.1 is a draft intended to make the idea testable and implementable by independent runtimes. See `ROADMAP.md`.
125
+
126
+
127
+ ## License
128
+
129
+ Licensed under Apache-2.0.
@@ -0,0 +1,3 @@
1
+ """Paxlet Core 0.1 reference runtime."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .errors import PaxletError
10
+ from .manifest import load_manifest, package_digest, validate_manifest
11
+ from .packing import pack as pack_paxlet
12
+ from .resolver import resolve, resolve_to_path
13
+ from .runtime import run_action
14
+
15
+
16
+ def _target(value: str, registry: str | None) -> Path:
17
+ if value.startswith("urn:paxlet:"):
18
+ return resolve_to_path(value, registry)
19
+ return Path(value)
20
+
21
+
22
+ def _json(value) -> None:
23
+ print(json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True))
24
+
25
+
26
+
27
+ def command_init(args) -> int:
28
+ target = Path(args.directory).expanduser().resolve()
29
+ if target.exists() and any(target.iterdir()) and not args.force:
30
+ raise PaxletError(f"directory is not empty: {target}; use --force to add starter files")
31
+ target.mkdir(parents=True, exist_ok=True)
32
+ name = args.name or target.name
33
+ slug = "".join(ch.lower() if ch.isalnum() else "-" for ch in name).strip("-")
34
+ while "--" in slug:
35
+ slug = slug.replace("--", "-")
36
+ urn = args.urn or f"urn:paxlet:local:{slug}"
37
+ manifest = {
38
+ "paxlet": "0.1",
39
+ "identity": {"urn": urn, "name": name, "version": "0.1.0"},
40
+ "bindings": [],
41
+ "actions": {
42
+ "hello": {
43
+ "description": "Your first Paxlet action.",
44
+ "runtime": {"type": "python", "entry": "hello.py"},
45
+ "input": {"type": "object", "properties": {"name": {"type": "string"}}},
46
+ "output": {"type": "object", "required": ["message"], "properties": {"message": {"type": "string"}}}
47
+ }
48
+ },
49
+ "resources": [],
50
+ "requires": [],
51
+ "permissions": {"filesystem": {"read": [], "write": []}, "network": [], "secrets": []},
52
+ "provenance": {"created_by": "paxlet init"}
53
+ }
54
+ manifest_file = target / "paxlet.json"
55
+ entry_file = target / "hello.py"
56
+ if not args.force and (manifest_file.exists() or entry_file.exists()):
57
+ raise PaxletError("starter files already exist; use --force to overwrite them")
58
+ manifest_file.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
59
+ entry_file.write_text(
60
+ 'import json, sys\n'
61
+ 'payload = json.load(sys.stdin)\n'
62
+ 'name = payload.get("name", "world")\n'
63
+ 'print(json.dumps({"message": f"Hello, {name}!"}))\n',
64
+ encoding="utf-8",
65
+ )
66
+ print(target)
67
+ print(f"next: paxlet run {target} hello --input '{{\"name\":\"Ada\"}}'")
68
+ return 0
69
+
70
+ def command_verify(args) -> int:
71
+ path = _target(args.target, args.registry)
72
+ manifest_file, manifest = load_manifest(path)
73
+ result = validate_manifest(manifest_file.parent, manifest)
74
+ payload = {"ok": result.ok, "errors": result.errors, "warnings": result.warnings}
75
+ if result.ok:
76
+ payload["identity"] = manifest["identity"]
77
+ payload["package_digest"] = package_digest(manifest_file.parent, manifest)
78
+ if args.json:
79
+ _json(payload)
80
+ else:
81
+ print("✓ valid Paxlet" if result.ok else "✗ invalid Paxlet")
82
+ for warning in result.warnings:
83
+ print(f"! {warning}")
84
+ for error in result.errors:
85
+ print(f"- {error}")
86
+ if result.ok:
87
+ print(f" {manifest['identity']['urn']} @ {manifest['identity']['version']}")
88
+ print(f" {payload['package_digest']}")
89
+ return 0 if result.ok else 1
90
+
91
+
92
+ def command_inspect(args) -> int:
93
+ path = _target(args.target, args.registry)
94
+ manifest_file, manifest = load_manifest(path)
95
+ result = validate_manifest(manifest_file.parent, manifest)
96
+ payload = {
97
+ "valid": result.ok,
98
+ "identity": manifest.get("identity"),
99
+ "bindings": manifest.get("bindings", []),
100
+ "actions": sorted(manifest.get("actions", {}).keys()),
101
+ "resources": manifest.get("resources", []),
102
+ "requires": manifest.get("requires", []),
103
+ "permissions": manifest.get("permissions", {}),
104
+ "package_digest": package_digest(manifest_file.parent, manifest) if result.ok else None,
105
+ "warnings": result.warnings,
106
+ "errors": result.errors,
107
+ }
108
+ _json(payload)
109
+ return 0 if result.ok else 1
110
+
111
+
112
+ def command_run(args) -> int:
113
+ path = _target(args.target, args.registry)
114
+ try:
115
+ payload = json.loads(args.input)
116
+ except json.JSONDecodeError as exc:
117
+ raise PaxletError(f"--input must be valid JSON: {exc}") from exc
118
+ output, receipt, receipt_path = run_action(
119
+ path,
120
+ args.action,
121
+ payload,
122
+ allow_secrets=args.allow_secret,
123
+ write_receipts=not args.no_receipt,
124
+ )
125
+ _json(output)
126
+ if receipt_path:
127
+ print(f"receipt: {receipt_path}", file=sys.stderr)
128
+ if args.show_receipt:
129
+ print(json.dumps(receipt, indent=2, ensure_ascii=False, sort_keys=True), file=sys.stderr)
130
+ return 0
131
+
132
+
133
+ def command_resolve(args) -> int:
134
+ locations = resolve(args.urn, args.registry)
135
+ _json({"urn": args.urn, "locations": locations})
136
+ return 0
137
+
138
+
139
+ def command_pack(args) -> int:
140
+ output = args.output or (Path(args.target).resolve().name + ".paxlet.zip")
141
+ path = pack_paxlet(_target(args.target, args.registry), output)
142
+ print(path)
143
+ return 0
144
+
145
+
146
+ def parser() -> argparse.ArgumentParser:
147
+ p = argparse.ArgumentParser(prog="paxlet", description="Paxlet Core 0.1 reference CLI")
148
+ p.add_argument("--version", action="version", version=__version__)
149
+ sub = p.add_subparsers(dest="command", required=True)
150
+
151
+ init = sub.add_parser("init", help="create a minimal runnable Paxlet")
152
+ init.add_argument("directory")
153
+ init.add_argument("--name")
154
+ init.add_argument("--urn")
155
+ init.add_argument("--force", action="store_true")
156
+ init.set_defaults(func=command_init)
157
+
158
+ verify = sub.add_parser("verify", help="validate a Paxlet manifest and package")
159
+ verify.add_argument("target")
160
+ verify.add_argument("--registry")
161
+ verify.add_argument("--json", action="store_true")
162
+ verify.set_defaults(func=command_verify)
163
+
164
+ inspect = sub.add_parser("inspect", help="show the public contract of a Paxlet")
165
+ inspect.add_argument("target")
166
+ inspect.add_argument("--registry")
167
+ inspect.set_defaults(func=command_inspect)
168
+
169
+ run = sub.add_parser("run", help="execute one action using JSON stdin/stdout protocol")
170
+ run.add_argument("target")
171
+ run.add_argument("action")
172
+ run.add_argument("--input", default="{}")
173
+ run.add_argument("--registry")
174
+ run.add_argument("--allow-secret", action="append", default=[])
175
+ run.add_argument("--no-receipt", action="store_true")
176
+ run.add_argument("--show-receipt", action="store_true")
177
+ run.set_defaults(func=command_run)
178
+
179
+ resolve_cmd = sub.add_parser("resolve", help="resolve a stable Paxlet URN to locations")
180
+ resolve_cmd.add_argument("urn")
181
+ resolve_cmd.add_argument("--registry")
182
+ resolve_cmd.set_defaults(func=command_resolve)
183
+
184
+ pack_cmd = sub.add_parser("pack", help="create a portable .paxlet.zip archive")
185
+ pack_cmd.add_argument("target")
186
+ pack_cmd.add_argument("-o", "--output")
187
+ pack_cmd.add_argument("--registry")
188
+ pack_cmd.set_defaults(func=command_pack)
189
+ return p
190
+
191
+
192
+ def main(argv: list[str] | None = None) -> int:
193
+ args = parser().parse_args(argv)
194
+ try:
195
+ return args.func(args)
196
+ except PaxletError as exc:
197
+ print(f"paxlet: {exc}", file=sys.stderr)
198
+ return 2
199
+
200
+
201
+ if __name__ == "__main__":
202
+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1
+ class PaxletError(Exception):
2
+ """Base error for the reference runtime."""
3
+
4
+
5
+ class ManifestError(PaxletError):
6
+ """Manifest is missing or invalid."""
7
+
8
+
9
+ class ResolutionError(PaxletError):
10
+ """An identity cannot be resolved to a usable location."""
11
+
12
+
13
+ class RuntimeError(PaxletError):
14
+ """An action could not be executed."""
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .errors import ManifestError
10
+
11
+ MANIFEST_NAME = "paxlet.json"
12
+ CORE_VERSION = "0.1"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ValidationResult:
17
+ ok: bool
18
+ errors: list[str]
19
+ warnings: list[str]
20
+
21
+
22
+ def canonical_json(value: Any) -> bytes:
23
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
24
+
25
+
26
+ def manifest_path(path: str | Path) -> Path:
27
+ candidate = Path(path).expanduser()
28
+ if candidate.is_dir():
29
+ candidate = candidate / MANIFEST_NAME
30
+ return candidate.resolve()
31
+
32
+
33
+ def load_manifest(path: str | Path) -> tuple[Path, dict[str, Any]]:
34
+ target = manifest_path(path)
35
+ if not target.exists():
36
+ raise ManifestError(f"manifest not found: {target}")
37
+ try:
38
+ data = json.loads(target.read_text(encoding="utf-8"))
39
+ except json.JSONDecodeError as exc:
40
+ raise ManifestError(f"invalid JSON in {target}: {exc}") from exc
41
+ if not isinstance(data, dict):
42
+ raise ManifestError("manifest root must be an object")
43
+ return target, data
44
+
45
+
46
+ def safe_path(root: Path, relative: str) -> Path:
47
+ if not isinstance(relative, str) or not relative or relative.startswith(("/", "\\")):
48
+ raise ManifestError(f"path must be a non-empty relative path: {relative!r}")
49
+ resolved_root = root.resolve()
50
+ resolved = (resolved_root / relative).resolve()
51
+ try:
52
+ resolved.relative_to(resolved_root)
53
+ except ValueError as exc:
54
+ raise ManifestError(f"path escapes package root: {relative}") from exc
55
+ return resolved
56
+
57
+
58
+ def _validate_schema_fragment(fragment: Any, label: str, errors: list[str]) -> None:
59
+ if fragment is None:
60
+ return
61
+ if not isinstance(fragment, dict):
62
+ errors.append(f"{label} must be an object")
63
+ return
64
+ if "type" in fragment and fragment["type"] not in {"object", "array", "string", "number", "integer", "boolean", "null"}:
65
+ errors.append(f"{label}.type is not a supported JSON type")
66
+ if fragment.get("type") == "object" and "properties" in fragment and not isinstance(fragment["properties"], dict):
67
+ errors.append(f"{label}.properties must be an object")
68
+ if "required" in fragment and not (isinstance(fragment["required"], list) and all(isinstance(v, str) for v in fragment["required"])):
69
+ errors.append(f"{label}.required must be an array of strings")
70
+
71
+
72
+ def validate_manifest(package_dir: Path, data: dict[str, Any], check_files: bool = True) -> ValidationResult:
73
+ errors: list[str] = []
74
+ warnings: list[str] = []
75
+
76
+ if data.get("paxlet") != CORE_VERSION:
77
+ errors.append(f"paxlet must be {CORE_VERSION!r}")
78
+
79
+ identity = data.get("identity")
80
+ if not isinstance(identity, dict):
81
+ errors.append("identity must be an object")
82
+ else:
83
+ urn = identity.get("urn")
84
+ if not isinstance(urn, str) or not urn.startswith("urn:paxlet:"):
85
+ errors.append("identity.urn must start with 'urn:paxlet:'")
86
+ for field in ("name", "version"):
87
+ if not isinstance(identity.get(field), str) or not identity[field].strip():
88
+ errors.append(f"identity.{field} must be a non-empty string")
89
+
90
+ bindings = data.get("bindings", [])
91
+ if not isinstance(bindings, list) or not all(isinstance(v, str) and ":" in v for v in bindings):
92
+ errors.append("bindings must be an array of URI strings")
93
+
94
+ actions = data.get("actions", {})
95
+ resources = data.get("resources", [])
96
+ if not isinstance(actions, dict):
97
+ errors.append("actions must be an object")
98
+ actions = {}
99
+ if not isinstance(resources, list) or not all(isinstance(v, str) for v in resources):
100
+ errors.append("resources must be an array of relative paths")
101
+ resources = []
102
+ if not actions and not resources:
103
+ errors.append("a Paxlet must contain at least one action or resource")
104
+
105
+ for name, action in actions.items():
106
+ label = f"actions.{name}"
107
+ if not isinstance(name, str) or not name.strip():
108
+ errors.append("action names must be non-empty strings")
109
+ continue
110
+ if not isinstance(action, dict):
111
+ errors.append(f"{label} must be an object")
112
+ continue
113
+ runtime = action.get("runtime")
114
+ if not isinstance(runtime, dict):
115
+ errors.append(f"{label}.runtime must be an object")
116
+ continue
117
+ runtime_type = runtime.get("type")
118
+ if runtime_type not in {"python", "command"}:
119
+ errors.append(f"{label}.runtime.type must be 'python' or 'command'")
120
+ if runtime_type == "python":
121
+ entry = runtime.get("entry")
122
+ if not isinstance(entry, str):
123
+ errors.append(f"{label}.runtime.entry must be a relative path")
124
+ else:
125
+ try:
126
+ path = safe_path(package_dir, entry)
127
+ if check_files and not path.is_file():
128
+ errors.append(f"{label}.runtime.entry not found: {entry}")
129
+ except ManifestError as exc:
130
+ errors.append(str(exc))
131
+ if runtime_type == "command":
132
+ argv = runtime.get("argv")
133
+ if not isinstance(argv, list) or not argv or not all(isinstance(v, str) for v in argv):
134
+ errors.append(f"{label}.runtime.argv must be a non-empty string array")
135
+ _validate_schema_fragment(action.get("input"), f"{label}.input", errors)
136
+ _validate_schema_fragment(action.get("output"), f"{label}.output", errors)
137
+
138
+ for resource in resources:
139
+ try:
140
+ path = safe_path(package_dir, resource)
141
+ if check_files and not path.exists():
142
+ errors.append(f"resource not found: {resource}")
143
+ except ManifestError as exc:
144
+ errors.append(str(exc))
145
+
146
+ requires = data.get("requires", [])
147
+ if not isinstance(requires, list) or not all(isinstance(v, str) and v.startswith("urn:paxlet:") for v in requires):
148
+ errors.append("requires must be an array of Paxlet URNs")
149
+
150
+ permissions = data.get("permissions", {})
151
+ if not isinstance(permissions, dict):
152
+ errors.append("permissions must be an object")
153
+ else:
154
+ fs = permissions.get("filesystem", {})
155
+ if not isinstance(fs, dict):
156
+ errors.append("permissions.filesystem must be an object")
157
+ else:
158
+ for mode in ("read", "write"):
159
+ vals = fs.get(mode, [])
160
+ if not isinstance(vals, list) or not all(isinstance(v, str) for v in vals):
161
+ errors.append(f"permissions.filesystem.{mode} must be an array of strings")
162
+ for key in ("network", "secrets"):
163
+ vals = permissions.get(key, [])
164
+ if not isinstance(vals, list) or not all(isinstance(v, str) for v in vals):
165
+ errors.append(f"permissions.{key} must be an array of strings")
166
+
167
+ provenance = data.get("provenance", {})
168
+ if provenance is not None and not isinstance(provenance, dict):
169
+ errors.append("provenance must be an object")
170
+
171
+ if permissions.get("network"):
172
+ warnings.append("reference runtime declares network permissions but does not enforce a network sandbox")
173
+ if isinstance(permissions.get("filesystem"), dict) and permissions["filesystem"].get("write"):
174
+ warnings.append("reference runtime declares filesystem write permissions but does not enforce an OS sandbox")
175
+
176
+ return ValidationResult(ok=not errors, errors=errors, warnings=warnings)
177
+
178
+
179
+ def collect_package_files(package_dir: Path, data: dict[str, Any]) -> list[Path]:
180
+ root = package_dir.resolve()
181
+ files: set[Path] = {root / MANIFEST_NAME}
182
+ for action in data.get("actions", {}).values():
183
+ runtime = action.get("runtime", {}) if isinstance(action, dict) else {}
184
+ if runtime.get("type") == "python" and isinstance(runtime.get("entry"), str):
185
+ files.add(safe_path(root, runtime["entry"]))
186
+ for relative in data.get("resources", []):
187
+ path = safe_path(root, relative)
188
+ if path.is_dir():
189
+ for child in path.rglob("*"):
190
+ if child.is_file():
191
+ files.add(child.resolve())
192
+ else:
193
+ files.add(path)
194
+ return sorted(files, key=lambda p: p.relative_to(root).as_posix())
195
+
196
+
197
+ def package_digest(package_dir: Path, data: dict[str, Any]) -> str:
198
+ root = package_dir.resolve()
199
+ digest = hashlib.sha256()
200
+ for path in collect_package_files(root, data):
201
+ relative = path.relative_to(root).as_posix().encode("utf-8")
202
+ digest.update(len(relative).to_bytes(4, "big"))
203
+ digest.update(relative)
204
+ content = path.read_bytes()
205
+ digest.update(len(content).to_bytes(8, "big"))
206
+ digest.update(content)
207
+ return "sha256:" + digest.hexdigest()
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import zipfile
5
+ from pathlib import Path
6
+
7
+ from .errors import ManifestError
8
+ from .manifest import collect_package_files, load_manifest, package_digest, validate_manifest
9
+
10
+
11
+ def pack(path: str | Path, output: str | Path) -> Path:
12
+ manifest_file, manifest = load_manifest(path)
13
+ root = manifest_file.parent
14
+ result = validate_manifest(root, manifest)
15
+ if not result.ok:
16
+ raise ManifestError("cannot pack invalid Paxlet: " + "; ".join(result.errors))
17
+ out = Path(output).expanduser().resolve()
18
+ out.parent.mkdir(parents=True, exist_ok=True)
19
+ metadata = {
20
+ "format": "paxlet-archive/0.1",
21
+ "identity": manifest["identity"],
22
+ "package_digest": package_digest(root, manifest),
23
+ }
24
+ with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf:
25
+ for file in collect_package_files(root, manifest):
26
+ zf.write(file, file.relative_to(root).as_posix())
27
+ info = zipfile.ZipInfo("PAXLET-METADATA.json", date_time=(1980, 1, 1, 0, 0, 0))
28
+ info.compress_type = zipfile.ZIP_DEFLATED
29
+ zf.writestr(info, json.dumps(metadata, indent=2, sort_keys=True) + "\n")
30
+ return out
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import socket
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .manifest import canonical_json
11
+
12
+
13
+ def utc_now() -> str:
14
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
15
+
16
+
17
+ def value_digest(value: Any) -> str:
18
+ return "sha256:" + hashlib.sha256(canonical_json(value)).hexdigest()
19
+
20
+
21
+ def new_receipt(*, manifest: dict[str, Any], package_digest: str, action: str, payload: Any, started: str, finished: str, exit_code: int, output: Any, secret_names: list[str]) -> dict[str, Any]:
22
+ identity = manifest["identity"]
23
+ return {
24
+ "receipt": "paxlet/0.1",
25
+ "identity": {"urn": identity["urn"], "version": identity["version"]},
26
+ "package_digest": package_digest,
27
+ "action": action,
28
+ "input_digest": value_digest(payload),
29
+ "output_digest": value_digest(output) if exit_code == 0 else None,
30
+ "runtime": "paxlet-python-reference/0.1.0",
31
+ "node": socket.gethostname(),
32
+ "started_at": started,
33
+ "finished_at": finished,
34
+ "exit_code": exit_code,
35
+ "granted_secret_names": sorted(secret_names),
36
+ "output": output,
37
+ }
38
+
39
+
40
+ def write_receipt(package_dir: Path, receipt: dict[str, Any]) -> Path:
41
+ directory = package_dir / ".paxlet" / "receipts"
42
+ directory.mkdir(parents=True, exist_ok=True)
43
+ safe_time = receipt["finished_at"].replace(":", "-").replace(".", "-")
44
+ path = directory / f"{safe_time}-{receipt['action']}.json"
45
+ path.write_text(json.dumps(receipt, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
46
+ return path
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from urllib.parse import unquote, urlparse
6
+
7
+ from .errors import ResolutionError
8
+
9
+ DEFAULT_REGISTRY = Path(__file__).resolve().parents[1] / "registry" / "local.json"
10
+
11
+
12
+ def load_registry(path: str | Path | None = None) -> tuple[Path, dict]:
13
+ target = Path(path).expanduser().resolve() if path else DEFAULT_REGISTRY
14
+ if not target.exists():
15
+ raise ResolutionError(f"registry not found: {target}")
16
+ try:
17
+ data = json.loads(target.read_text(encoding="utf-8"))
18
+ except json.JSONDecodeError as exc:
19
+ raise ResolutionError(f"invalid registry JSON: {exc}") from exc
20
+ if data.get("registry") != "paxlet/0.1" or not isinstance(data.get("entries"), dict):
21
+ raise ResolutionError("unsupported registry format")
22
+ return target, data
23
+
24
+
25
+ def resolve(urn: str, registry_path: str | Path | None = None) -> list[dict]:
26
+ target, data = load_registry(registry_path)
27
+ entries = data["entries"].get(urn, [])
28
+ if not entries:
29
+ raise ResolutionError(f"URN not found: {urn}")
30
+ out: list[dict] = []
31
+ for item in entries:
32
+ if not isinstance(item, dict) or not isinstance(item.get("uri"), str):
33
+ continue
34
+ uri = item["uri"]
35
+ parsed = urlparse(uri)
36
+ if parsed.scheme == "file" and not parsed.netloc:
37
+ raw_path = unquote(parsed.path)
38
+ p = Path(raw_path)
39
+ if not p.is_absolute():
40
+ p = (target.parent / p).resolve()
41
+ uri = p.as_uri()
42
+ out.append({**item, "uri": uri})
43
+ if not out:
44
+ raise ResolutionError(f"URN has no usable locations: {urn}")
45
+ return out
46
+
47
+
48
+ def uri_to_path(uri: str) -> Path:
49
+ parsed = urlparse(uri)
50
+ if parsed.scheme != "file":
51
+ raise ResolutionError(f"reference runtime can execute only file: locations, got {parsed.scheme or 'no scheme'}")
52
+ if parsed.netloc not in ("", "localhost"):
53
+ raise ResolutionError("remote file:// hosts are not supported")
54
+ return Path(unquote(parsed.path)).resolve()
55
+
56
+
57
+ def resolve_to_path(urn: str, registry_path: str | Path | None = None) -> Path:
58
+ locations = resolve(urn, registry_path)
59
+ for item in locations:
60
+ try:
61
+ return uri_to_path(item["uri"])
62
+ except ResolutionError:
63
+ continue
64
+ raise ResolutionError(f"no executable file: location for {urn}")
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .errors import RuntimeError
11
+ from .manifest import load_manifest, package_digest, validate_manifest
12
+ from .receipt import new_receipt, utc_now, write_receipt
13
+ from .schema import validate_value
14
+
15
+ SAFE_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR"]
16
+
17
+
18
+ def _environment(manifest: dict[str, Any], package_dir: Path, action: str, allow_secrets: list[str]) -> tuple[dict[str, str], list[str]]:
19
+ env = {key: os.environ[key] for key in SAFE_ENV_KEYS if key in os.environ}
20
+ declared = set(manifest.get("permissions", {}).get("secrets", []))
21
+ granted: list[str] = []
22
+ for name in allow_secrets:
23
+ if name not in declared:
24
+ raise RuntimeError(f"secret {name!r} is not declared by the Paxlet")
25
+ if name not in os.environ:
26
+ raise RuntimeError(f"secret {name!r} is not present in the current environment")
27
+ env[name] = os.environ[name]
28
+ granted.append(name)
29
+ identity = manifest["identity"]
30
+ env.update({
31
+ "PAXLET_URN": identity["urn"],
32
+ "PAXLET_VERSION": identity["version"],
33
+ "PAXLET_ACTION": action,
34
+ "PAXLET_ROOT": str(package_dir),
35
+ })
36
+ return env, granted
37
+
38
+
39
+ def run_action(path: str | Path, action_name: str, payload: Any, *, allow_secrets: list[str] | None = None, write_receipts: bool = True) -> tuple[Any, dict[str, Any], Path | None]:
40
+ manifest_file, manifest = load_manifest(path)
41
+ package_dir = manifest_file.parent
42
+ result = validate_manifest(package_dir, manifest)
43
+ if not result.ok:
44
+ raise RuntimeError("invalid Paxlet: " + "; ".join(result.errors))
45
+
46
+ action = manifest.get("actions", {}).get(action_name)
47
+ if not isinstance(action, dict):
48
+ raise RuntimeError(f"action not found: {action_name}")
49
+ validate_value(payload, action.get("input"), "input")
50
+
51
+ runtime = action["runtime"]
52
+ if runtime["type"] == "python":
53
+ command = [sys.executable, runtime["entry"]]
54
+ else:
55
+ command = runtime["argv"]
56
+
57
+ env, granted = _environment(manifest, package_dir, action_name, allow_secrets or [])
58
+ started = utc_now()
59
+ completed = subprocess.run(
60
+ command,
61
+ cwd=package_dir,
62
+ input=json.dumps(payload, ensure_ascii=False),
63
+ text=True,
64
+ capture_output=True,
65
+ env=env,
66
+ check=False,
67
+ )
68
+ finished = utc_now()
69
+
70
+ if completed.returncode != 0:
71
+ failure = {
72
+ "error": "action_failed",
73
+ "message": completed.stderr.strip() or completed.stdout.strip() or f"exit code {completed.returncode}",
74
+ }
75
+ receipt = new_receipt(
76
+ manifest=manifest,
77
+ package_digest=package_digest(package_dir, manifest),
78
+ action=action_name,
79
+ payload=payload,
80
+ started=started,
81
+ finished=finished,
82
+ exit_code=completed.returncode,
83
+ output=failure,
84
+ secret_names=granted,
85
+ )
86
+ receipt_path = write_receipt(package_dir, receipt) if write_receipts else None
87
+ raise RuntimeError(f"action failed ({completed.returncode}): {failure['message']}; receipt={receipt_path}")
88
+
89
+ try:
90
+ output = json.loads(completed.stdout)
91
+ except json.JSONDecodeError as exc:
92
+ raise RuntimeError(f"action must write exactly one JSON value to stdout; got: {completed.stdout[:200]!r}") from exc
93
+ validate_value(output, action.get("output"), "output")
94
+
95
+ receipt = new_receipt(
96
+ manifest=manifest,
97
+ package_digest=package_digest(package_dir, manifest),
98
+ action=action_name,
99
+ payload=payload,
100
+ started=started,
101
+ finished=finished,
102
+ exit_code=0,
103
+ output=output,
104
+ secret_names=granted,
105
+ )
106
+ receipt_path = write_receipt(package_dir, receipt) if write_receipts else None
107
+ return output, receipt, receipt_path
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .errors import RuntimeError
6
+
7
+
8
+ def _matches_type(value: Any, expected: str) -> bool:
9
+ if expected == "object":
10
+ return isinstance(value, dict)
11
+ if expected == "array":
12
+ return isinstance(value, list)
13
+ if expected == "string":
14
+ return isinstance(value, str)
15
+ if expected == "number":
16
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
17
+ if expected == "integer":
18
+ return isinstance(value, int) and not isinstance(value, bool)
19
+ if expected == "boolean":
20
+ return isinstance(value, bool)
21
+ if expected == "null":
22
+ return value is None
23
+ return True
24
+
25
+
26
+ def validate_value(value: Any, schema: dict[str, Any] | None, label: str = "value") -> None:
27
+ """Validate the intentionally small JSON-Schema subset in Core 0.1."""
28
+ if not schema:
29
+ return
30
+ expected = schema.get("type")
31
+ if expected and not _matches_type(value, expected):
32
+ raise RuntimeError(f"{label} must be {expected}")
33
+ if expected == "object" and isinstance(value, dict):
34
+ required = schema.get("required", [])
35
+ for key in required:
36
+ if key not in value:
37
+ raise RuntimeError(f"{label}.{key} is required")
38
+ for key, child_schema in schema.get("properties", {}).items():
39
+ if key in value and isinstance(child_schema, dict):
40
+ validate_value(value[key], child_schema, f"{label}.{key}")
41
+ if expected == "array" and isinstance(value, list) and isinstance(schema.get("items"), dict):
42
+ for index, item in enumerate(value):
43
+ validate_value(item, schema["items"], f"{label}[{index}]")
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: paxlet
3
+ Version: 0.1.1
4
+ Summary: Small, addressable packages of resources and actions for connected systems.
5
+ Author: Paxlet contributors
6
+ Author-email: Tom Sapletta <tom@sapletta.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+
11
+ # paxlet
12
+
13
+
14
+ ## AI Cost Tracking
15
+
16
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.1-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.05-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-1.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
18
+
19
+ - 🤖 **LLM usage:** $0.0466 (1 commits)
20
+ - 👤 **Human dev:** ~$100 (1.0h @ $100/h, 30min dedup)
21
+
22
+ Generated on 2026-09-22 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
23
+
24
+ ---
25
+
26
+
27
+
28
+ **Build small. Connect everything.**
29
+
30
+ Paxlet Core is a deliberately small contract for packaging **resources and actions** as addressable capabilities.
31
+
32
+ A Paxlet has a stable identity, a version, clear inputs/outputs, explicit dependencies and requested permissions. A node can resolve it, verify it, run it and produce a receipt.
33
+
34
+ ```text
35
+ URN identity
36
+
37
+
38
+ ┌──────────── Paxlet ────────────┐
39
+ │ resources │
40
+ │ actions │
41
+ │ dependencies │
42
+ │ permissions │
43
+ │ provenance │
44
+ └───────────────────────────────┘
45
+
46
+
47
+ URI locations → Node → Receipt
48
+ ```
49
+
50
+ ## Start in five minutes
51
+
52
+ Requires Python 3.11+ and no third-party runtime dependencies. You can run it directly after cloning:
53
+
54
+ ```bash
55
+ ./bin/paxlet init /tmp/my-first
56
+ ./bin/paxlet run /tmp/my-first hello --input '{"name":"Ada"}'
57
+
58
+ ./bin/paxlet verify examples/hello
59
+ ./bin/paxlet run examples/hello hello --input '{"name":"Ada"}'
60
+ ```
61
+
62
+ Optional developer install:
63
+
64
+ ```bash
65
+ python -m venv .venv
66
+ . .venv/bin/activate
67
+ pip install -e .
68
+ ```
69
+
70
+ Or address the same capability by identity:
71
+
72
+ ```bash
73
+ ./bin/paxlet resolve urn:paxlet:example:hello
74
+ ./bin/paxlet run urn:paxlet:example:hello hello --input '{"name":"Ada"}'
75
+ ```
76
+
77
+ Try a science-oriented example:
78
+
79
+ ```bash
80
+ ./bin/paxlet run examples/science-fasta-gc analyze --input '{}'
81
+ ./bin/paxlet run examples/statistics-mean mean --input '{"values":[1,2,4,8]}'
82
+ ```
83
+
84
+ ## CLI
85
+
86
+ ```text
87
+ paxlet init DIRECTORY
88
+ paxlet verify PATH|URN
89
+ paxlet inspect PATH|URN
90
+ paxlet resolve URN
91
+ paxlet run PATH|URN ACTION --input JSON
92
+ paxlet pack PATH|URN -o package.paxlet.zip
93
+ ```
94
+
95
+ ## Why Core is small
96
+
97
+ Paxlet is not an AI-agent framework. It is the portable unit that larger systems can use.
98
+
99
+ ```text
100
+ Paxlet Core package + identity + contract
101
+ Paxlet Node resolution + grants + execution + receipts
102
+ Paxlet Network registries + peers + distribution
103
+ Taskand planning + orchestration + adaptive-system showcase
104
+ ```
105
+
106
+ Taskand can demonstrate planners, processes, federation, mesh, Digital Twin, MCP, governance and recovery without making those features mandatory for every Paxlet implementation.
107
+
108
+ ## Repository map
109
+
110
+ ```text
111
+ SPEC.md normative Core 0.1 draft
112
+ schemas/ manifest + receipt schemas
113
+ paxlet/ tiny Python reference runtime
114
+ examples/ five understandable capabilities
115
+ registry/local.json local URN → URI resolver example
116
+ conformance/ implementation compatibility runner
117
+ docs/ addressing, security, receipts, layers
118
+ integrations/taskand/ how Taskand fits as the showcase
119
+ ```
120
+
121
+ ## Conformance
122
+
123
+ ```bash
124
+ python -m unittest discover -s tests -v
125
+ python conformance/run.py
126
+ ```
127
+
128
+ ## Important security note
129
+
130
+ The reference runtime is intentionally educational. It validates package paths and exposes secrets only after explicit user grant, but it **does not enforce filesystem/network permissions with an OS sandbox**. Production nodes should use containers, VMs or OS policy for enforcement. See `docs/security.md`.
131
+
132
+ ## Status
133
+
134
+ Core 0.1 is a draft intended to make the idea testable and implementable by independent runtimes. See `ROADMAP.md`.
135
+
136
+
137
+ ## License
138
+
139
+ Licensed under Apache-2.0.
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ paxlet/__init__.py
4
+ paxlet/__main__.py
5
+ paxlet/cli.py
6
+ paxlet/errors.py
7
+ paxlet/manifest.py
8
+ paxlet/packing.py
9
+ paxlet/receipt.py
10
+ paxlet/resolver.py
11
+ paxlet/runtime.py
12
+ paxlet/schema.py
13
+ paxlet.egg-info/PKG-INFO
14
+ paxlet.egg-info/SOURCES.txt
15
+ paxlet.egg-info/dependency_links.txt
16
+ paxlet.egg-info/entry_points.txt
17
+ paxlet.egg-info/top_level.txt
18
+ tests/test_core.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ paxlet = paxlet.cli:main
@@ -0,0 +1 @@
1
+ paxlet
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "paxlet"
7
+ version = "0.1.1"
8
+ description = "Small, addressable packages of resources and actions for connected systems."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{name = "Paxlet contributors"}, {name = "Tom Sapletta",email = "tom@sapletta.com"}]
12
+ dependencies = []
13
+ license = "Apache-2.0"
14
+
15
+ [project.scripts]
16
+ paxlet = "paxlet.cli:main"
17
+
18
+ [tool.setuptools]
19
+ packages = ["paxlet"]
20
+
21
+ [tool.costs]
22
+ # AI Cost tracking configuration
23
+ badge = true
24
+ update_readme = true
25
+ readme_path = "README.md"
26
+ default_model = "openrouter/qwen/qwen3-coder-next"
27
+ analysis_mode = "byok"
28
+ full_history = true
29
+ max_commits = 500
30
+
31
+ # Cost thresholds for badge colors (USD)
32
+ badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
paxlet-0.1.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,53 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from paxlet.errors import ManifestError
9
+ from paxlet.manifest import load_manifest, validate_manifest
10
+ from paxlet.packing import pack
11
+ from paxlet.resolver import resolve_to_path
12
+ from paxlet.runtime import run_action
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+
16
+
17
+ class CoreTests(unittest.TestCase):
18
+ def test_init_creates_runnable_package(self):
19
+ with tempfile.TemporaryDirectory() as td:
20
+ target = Path(td) / "first"
21
+ created = subprocess.run([sys.executable, "-m", "paxlet.cli", "init", str(target)], cwd=ROOT, text=True, capture_output=True)
22
+ self.assertEqual(created.returncode, 0, created.stderr)
23
+ output, _, _ = run_action(target, "hello", {"name": "Ada"}, write_receipts=False)
24
+ self.assertEqual(output["message"], "Hello, Ada!")
25
+
26
+ def test_hello_validates(self):
27
+ mf, data = load_manifest(ROOT / "examples/hello")
28
+ self.assertTrue(validate_manifest(mf.parent, data).ok)
29
+
30
+ def test_path_escape_rejected(self):
31
+ mf, data = load_manifest(ROOT / "tests/fixtures/invalid-escape")
32
+ result = validate_manifest(mf.parent, data)
33
+ self.assertFalse(result.ok)
34
+ self.assertTrue(any("escapes package root" in e for e in result.errors))
35
+
36
+ def test_run_hello_and_receipt(self):
37
+ output, receipt, _ = run_action(ROOT / "examples/hello", "hello", {"name": "Ada"}, write_receipts=False)
38
+ self.assertEqual(output, {"message": "Hello, Ada!"})
39
+ self.assertEqual(receipt["identity"]["urn"], "urn:paxlet:example:hello")
40
+ self.assertTrue(receipt["package_digest"].startswith("sha256:"))
41
+
42
+ def test_resolver(self):
43
+ path = resolve_to_path("urn:paxlet:science:fasta-gc", ROOT / "registry/local.json")
44
+ self.assertEqual(path, (ROOT / "examples/science-fasta-gc").resolve())
45
+
46
+ def test_pack(self):
47
+ with tempfile.TemporaryDirectory() as td:
48
+ out = pack(ROOT / "examples/hello", Path(td) / "hello.paxlet.zip")
49
+ self.assertTrue(out.exists())
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()