tenaxisos-sdk 0.2.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.
- tenaxisos_sdk-0.2.0/LICENSE +21 -0
- tenaxisos_sdk-0.2.0/PKG-INFO +131 -0
- tenaxisos_sdk-0.2.0/README.md +108 -0
- tenaxisos_sdk-0.2.0/pyproject.toml +55 -0
- tenaxisos_sdk-0.2.0/setup.cfg +4 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/__init__.py +14 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/__init__.py +20 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/base.py +277 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/context.py +132 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/findings.py +96 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/manifest.py +158 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/cli/__init__.py +1 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/cli/main.py +956 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/__init__.py +9 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/manifest.py +194 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schema.py +53 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schemas/manifest.v0.schema.json +198 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schemas/playbook.v0.schema.json +96 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/signing.py +106 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/host/__init__.py +111 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/importers/__init__.py +10 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/importers/n8n.py +313 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/__init__.py +5 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/__init__.py +3 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/base.py +62 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/langchain.py +161 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/testing/__init__.py +3 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/testing/local_host.py +90 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/__init__.py +12 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/base.py +66 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/manifest.py +105 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/schema.py +44 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/stdio.py +57 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/PKG-INFO +131 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/SOURCES.txt +52 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/dependency_links.txt +1 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/entry_points.txt +2 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/requires.txt +16 -0
- tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/top_level.txt +1 -0
- tenaxisos_sdk-0.2.0/tests/test_agents_authoring.py +156 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_agent_build.py +70 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_mcp.py +107 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_playbook.py +70 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_publish.py +163 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_run.py +89 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_verify.py +93 -0
- tenaxisos_sdk-0.2.0/tests/test_cli_workflow_import.py +61 -0
- tenaxisos_sdk-0.2.0/tests/test_langchain_adapter.py +124 -0
- tenaxisos_sdk-0.2.0/tests/test_manifest.py +203 -0
- tenaxisos_sdk-0.2.0/tests/test_mcp_tools.py +119 -0
- tenaxisos_sdk-0.2.0/tests/test_n8n_importer.py +125 -0
- tenaxisos_sdk-0.2.0/tests/test_native_agent_build.py +68 -0
- tenaxisos_sdk-0.2.0/tests/test_sdk_compatibility.py +75 -0
- tenaxisos_sdk-0.2.0/tests/test_signing_parity.py +61 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tenaxisai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tenaxisos-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Build, validate, sign, and publish TenaxisOS packages and playbooks
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: typer>=0.12
|
|
10
|
+
Requires-Dist: rich>=13
|
|
11
|
+
Requires-Dist: jsonschema>=4.23
|
|
12
|
+
Requires-Dist: cryptography>=42
|
|
13
|
+
Requires-Dist: httpx>=0.27
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
18
|
+
Provides-Extra: langchain
|
|
19
|
+
Requires-Dist: langchain-core>=0.3; extra == "langchain"
|
|
20
|
+
Provides-Extra: mcp
|
|
21
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# tenaxisos-sdk
|
|
25
|
+
|
|
26
|
+
Python SDK and CLI for building, testing, signing, and publishing TenaxisOS
|
|
27
|
+
packages — agents, MCP servers, and playbooks that install into a TenaxisOS
|
|
28
|
+
kernel and can be shared through the Tenaxis Hub.
|
|
29
|
+
|
|
30
|
+
## What this is
|
|
31
|
+
|
|
32
|
+
`tenaxisos-sdk` is the toolkit for building community capabilities for
|
|
33
|
+
TenaxisOS:
|
|
34
|
+
|
|
35
|
+
- **Author** agents and MCP servers as plain Python, using `Agent` and
|
|
36
|
+
`MCPServer` base classes with host-brokered LLM/tool/file access — no
|
|
37
|
+
network calls, secrets, or infrastructure of your own to manage.
|
|
38
|
+
- **Test locally** against `LocalMockHost`, a fake host that lets you drive
|
|
39
|
+
and assert on agent/tool behavior entirely offline.
|
|
40
|
+
- **Build** a signed, versioned package manifest from your code.
|
|
41
|
+
- **Publish** to the Tenaxis Hub (or import straight into a kernel you
|
|
42
|
+
control for local development) so others can discover and install it.
|
|
43
|
+
|
|
44
|
+
## Getting started
|
|
45
|
+
|
|
46
|
+
The fastest way in is [`examples/`](examples/) — three runnable,
|
|
47
|
+
heavily-commented examples, one per authoring surface (native agent, MCP
|
|
48
|
+
server, LangChain framework adapter). Each has its own tests you can run
|
|
49
|
+
immediately, no kernel or network required:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install -e ".[dev,mcp,langchain]"
|
|
53
|
+
pytest examples/minimal_text_agent/test_agent.py -v
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See [`examples/README.md`](examples/README.md) for which one to start with.
|
|
57
|
+
|
|
58
|
+
## Documentation
|
|
59
|
+
|
|
60
|
+
Prose tutorials — go here once you know which capability you're building
|
|
61
|
+
and want more depth than the examples give:
|
|
62
|
+
|
|
63
|
+
- [Writing a native agent](docs/native_agent_tutorial.md)
|
|
64
|
+
- [Writing an MCP server](docs/mcp_server_tutorial.md)
|
|
65
|
+
- [Local testing with `LocalMockHost`](docs/local_testing.md)
|
|
66
|
+
- [Publishing and installing](docs/publish_and_install.md)
|
|
67
|
+
- [Runtime permission outcomes](docs/runtime_outcomes.md)
|
|
68
|
+
|
|
69
|
+
## What this contains
|
|
70
|
+
|
|
71
|
+
| Directory | Purpose |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `src/tenaxisos_sdk/core/` | Manifest validation, JSON Schema loading, Ed25519 signing/verification |
|
|
74
|
+
| `src/tenaxisos_sdk/agents/` | `Agent`/`RunContext`/`Finding` authoring surface, manifest generation |
|
|
75
|
+
| `src/tenaxisos_sdk/host/` | The host-brokered contract (`Host` protocol, `ToolOutcome`) every agent runs against |
|
|
76
|
+
| `src/tenaxisos_sdk/testing/` | `LocalMockHost` — no-kernel local test host |
|
|
77
|
+
| `src/tenaxisos_sdk/tools/` | `Tool`/`MCPServer` authoring surface, MCP manifest generation, `[mcp]` extra for the real stdio protocol bridge |
|
|
78
|
+
| `src/tenaxisos_sdk/cli/` | The `tenaxis` CLI: `validate`, `build`, `sign`, `verify`, `test`, `publish`, `import`, `keygen`, `run`, `agent build`, `mcp init/test/build`, `workflow import` |
|
|
79
|
+
| `src/tenaxisos_sdk/runtime/adapters/` | Agent framework adapters (LangChain today, `[langchain]` optional extra) |
|
|
80
|
+
| `src/tenaxisos_sdk/importers/` | External-workflow importers (n8n today) used by `tenaxis workflow import` |
|
|
81
|
+
|
|
82
|
+
This package is **not yet published to PyPI** — install it from source
|
|
83
|
+
(below) in the meantime.
|
|
84
|
+
|
|
85
|
+
## Prerequisites
|
|
86
|
+
|
|
87
|
+
- **Python 3.11 or 3.12**.
|
|
88
|
+
- No database, no server, no `.env` file — the CLI is stateless and takes
|
|
89
|
+
everything (hub URL, API keys, key paths) as command-line flags.
|
|
90
|
+
|
|
91
|
+
## Installation
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python3 -m venv .venv && source .venv/bin/activate # optional but recommended
|
|
95
|
+
pip install -e ".[dev]" # editable install + pytest/ruff; drop [dev] for CLI-only use
|
|
96
|
+
tenaxis --help
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Add `[langchain]` (e.g. `pip install -e ".[dev,langchain]"`) if you need the
|
|
100
|
+
LangChain agent adapter, or `[mcp]` if you need to run an MCP server as a
|
|
101
|
+
real stdio process — both are kept optional so the base SDK never requires
|
|
102
|
+
heavy adapter-specific dependencies.
|
|
103
|
+
|
|
104
|
+
## Everyday usage
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
tenaxis keygen # once — generates an Ed25519 signing key pair
|
|
108
|
+
tenaxis validate <manifest.json> # schema + structural validation
|
|
109
|
+
tenaxis test <manifest.json> # runs the package's declared tests
|
|
110
|
+
tenaxis build <manifest-dir> --out <out.package.json> # produces a distributable package artifact
|
|
111
|
+
tenaxis sign <manifest.json> --key <private-key-path> # signing ceremony (human-reviewed, not run in CI)
|
|
112
|
+
tenaxis verify <manifest.json> --pubkey <public-key-b64> [--require]
|
|
113
|
+
tenaxis publish <manifest.json> --hub <hub-url> --api-key <publisher-api-key>
|
|
114
|
+
tenaxis import <n8n-export.json> --out <package-dir> # n8n → native workflow package
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`tenaxis keygen` writes your private signing key to disk (permissions locked
|
|
118
|
+
to owner-only) — keep it out of version control and treat it like any other
|
|
119
|
+
private key material; the CLI never transmits or stores it for you.
|
|
120
|
+
|
|
121
|
+
### Tests
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pip install -e ".[dev]"
|
|
125
|
+
pytest tests/ -v
|
|
126
|
+
ruff check src/ tests/
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# tenaxisos-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK and CLI for building, testing, signing, and publishing TenaxisOS
|
|
4
|
+
packages — agents, MCP servers, and playbooks that install into a TenaxisOS
|
|
5
|
+
kernel and can be shared through the Tenaxis Hub.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
`tenaxisos-sdk` is the toolkit for building community capabilities for
|
|
10
|
+
TenaxisOS:
|
|
11
|
+
|
|
12
|
+
- **Author** agents and MCP servers as plain Python, using `Agent` and
|
|
13
|
+
`MCPServer` base classes with host-brokered LLM/tool/file access — no
|
|
14
|
+
network calls, secrets, or infrastructure of your own to manage.
|
|
15
|
+
- **Test locally** against `LocalMockHost`, a fake host that lets you drive
|
|
16
|
+
and assert on agent/tool behavior entirely offline.
|
|
17
|
+
- **Build** a signed, versioned package manifest from your code.
|
|
18
|
+
- **Publish** to the Tenaxis Hub (or import straight into a kernel you
|
|
19
|
+
control for local development) so others can discover and install it.
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
The fastest way in is [`examples/`](examples/) — three runnable,
|
|
24
|
+
heavily-commented examples, one per authoring surface (native agent, MCP
|
|
25
|
+
server, LangChain framework adapter). Each has its own tests you can run
|
|
26
|
+
immediately, no kernel or network required:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e ".[dev,mcp,langchain]"
|
|
30
|
+
pytest examples/minimal_text_agent/test_agent.py -v
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [`examples/README.md`](examples/README.md) for which one to start with.
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Prose tutorials — go here once you know which capability you're building
|
|
38
|
+
and want more depth than the examples give:
|
|
39
|
+
|
|
40
|
+
- [Writing a native agent](docs/native_agent_tutorial.md)
|
|
41
|
+
- [Writing an MCP server](docs/mcp_server_tutorial.md)
|
|
42
|
+
- [Local testing with `LocalMockHost`](docs/local_testing.md)
|
|
43
|
+
- [Publishing and installing](docs/publish_and_install.md)
|
|
44
|
+
- [Runtime permission outcomes](docs/runtime_outcomes.md)
|
|
45
|
+
|
|
46
|
+
## What this contains
|
|
47
|
+
|
|
48
|
+
| Directory | Purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `src/tenaxisos_sdk/core/` | Manifest validation, JSON Schema loading, Ed25519 signing/verification |
|
|
51
|
+
| `src/tenaxisos_sdk/agents/` | `Agent`/`RunContext`/`Finding` authoring surface, manifest generation |
|
|
52
|
+
| `src/tenaxisos_sdk/host/` | The host-brokered contract (`Host` protocol, `ToolOutcome`) every agent runs against |
|
|
53
|
+
| `src/tenaxisos_sdk/testing/` | `LocalMockHost` — no-kernel local test host |
|
|
54
|
+
| `src/tenaxisos_sdk/tools/` | `Tool`/`MCPServer` authoring surface, MCP manifest generation, `[mcp]` extra for the real stdio protocol bridge |
|
|
55
|
+
| `src/tenaxisos_sdk/cli/` | The `tenaxis` CLI: `validate`, `build`, `sign`, `verify`, `test`, `publish`, `import`, `keygen`, `run`, `agent build`, `mcp init/test/build`, `workflow import` |
|
|
56
|
+
| `src/tenaxisos_sdk/runtime/adapters/` | Agent framework adapters (LangChain today, `[langchain]` optional extra) |
|
|
57
|
+
| `src/tenaxisos_sdk/importers/` | External-workflow importers (n8n today) used by `tenaxis workflow import` |
|
|
58
|
+
|
|
59
|
+
This package is **not yet published to PyPI** — install it from source
|
|
60
|
+
(below) in the meantime.
|
|
61
|
+
|
|
62
|
+
## Prerequisites
|
|
63
|
+
|
|
64
|
+
- **Python 3.11 or 3.12**.
|
|
65
|
+
- No database, no server, no `.env` file — the CLI is stateless and takes
|
|
66
|
+
everything (hub URL, API keys, key paths) as command-line flags.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python3 -m venv .venv && source .venv/bin/activate # optional but recommended
|
|
72
|
+
pip install -e ".[dev]" # editable install + pytest/ruff; drop [dev] for CLI-only use
|
|
73
|
+
tenaxis --help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Add `[langchain]` (e.g. `pip install -e ".[dev,langchain]"`) if you need the
|
|
77
|
+
LangChain agent adapter, or `[mcp]` if you need to run an MCP server as a
|
|
78
|
+
real stdio process — both are kept optional so the base SDK never requires
|
|
79
|
+
heavy adapter-specific dependencies.
|
|
80
|
+
|
|
81
|
+
## Everyday usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
tenaxis keygen # once — generates an Ed25519 signing key pair
|
|
85
|
+
tenaxis validate <manifest.json> # schema + structural validation
|
|
86
|
+
tenaxis test <manifest.json> # runs the package's declared tests
|
|
87
|
+
tenaxis build <manifest-dir> --out <out.package.json> # produces a distributable package artifact
|
|
88
|
+
tenaxis sign <manifest.json> --key <private-key-path> # signing ceremony (human-reviewed, not run in CI)
|
|
89
|
+
tenaxis verify <manifest.json> --pubkey <public-key-b64> [--require]
|
|
90
|
+
tenaxis publish <manifest.json> --hub <hub-url> --api-key <publisher-api-key>
|
|
91
|
+
tenaxis import <n8n-export.json> --out <package-dir> # n8n → native workflow package
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`tenaxis keygen` writes your private signing key to disk (permissions locked
|
|
95
|
+
to owner-only) — keep it out of version control and treat it like any other
|
|
96
|
+
private key material; the CLI never transmits or stores it for you.
|
|
97
|
+
|
|
98
|
+
### Tests
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pip install -e ".[dev]"
|
|
102
|
+
pytest tests/ -v
|
|
103
|
+
ruff check src/ tests/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tenaxisos-sdk"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Build, validate, sign, and publish TenaxisOS packages and playbooks"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer>=0.12",
|
|
14
|
+
"rich>=13",
|
|
15
|
+
"jsonschema>=4.23",
|
|
16
|
+
"cryptography>=42",
|
|
17
|
+
"httpx>=0.27",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
tenaxis = "tenaxisos_sdk.cli.main:app"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest>=8",
|
|
26
|
+
"pytest-asyncio>=0.24",
|
|
27
|
+
"ruff>=0.4",
|
|
28
|
+
]
|
|
29
|
+
# LangChain agent adapter — kept optional so the base SDK
|
|
30
|
+
# (build/sign/validate/publish) never requires heavy adapter-specific
|
|
31
|
+
# dependencies.
|
|
32
|
+
langchain = [
|
|
33
|
+
"langchain-core>=0.3",
|
|
34
|
+
]
|
|
35
|
+
# MCP server authoring. Only needed to actually run
|
|
36
|
+
# `tenaxisos_sdk.tools.stdio.run_stdio`; declaring/testing tools via
|
|
37
|
+
# `tenaxisos_sdk.tools.MCPServer` needs no extra.
|
|
38
|
+
mcp = [
|
|
39
|
+
"mcp>=1.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
"tenaxisos_sdk.core" = ["schemas/*.json"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
asyncio_mode = "auto"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""TenaxisOS SDK — build, validate, sign, and publish packages."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
# The SDK authoring/runtime host contract version (`compatibility.sdk_api` in
|
|
6
|
+
# a package manifest). Distinct axis from `__version__`: this only changes
|
|
7
|
+
# when the *shape* of `tenaxisos_sdk.agents`/`tenaxisos_sdk.host` changes in
|
|
8
|
+
# a way that could break a package built against an older contract (a new
|
|
9
|
+
# required `Host` method, a renamed `Agent` method, a changed `ToolOutcome`
|
|
10
|
+
# member) — not on every release. Bump both together only when a release
|
|
11
|
+
# happens to also be a contract change. The kernel checks a package's
|
|
12
|
+
# declared `compatibility.sdk_api` range against this value before running
|
|
13
|
+
# packages that rely on the public SDK runtime contract.
|
|
14
|
+
SDK_API_VERSION = "0.2.0"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from tenaxisos_sdk.agents.base import Agent, ProgressCallback
|
|
2
|
+
from tenaxisos_sdk.agents.context import RunContext
|
|
3
|
+
from tenaxisos_sdk.agents.findings import (
|
|
4
|
+
BASE_INPUT_SCHEMA,
|
|
5
|
+
FINDING_SCHEMA,
|
|
6
|
+
Finding,
|
|
7
|
+
validate_findings,
|
|
8
|
+
)
|
|
9
|
+
from tenaxisos_sdk.agents.manifest import generate_agent_payload
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Agent",
|
|
13
|
+
"ProgressCallback",
|
|
14
|
+
"RunContext",
|
|
15
|
+
"Finding",
|
|
16
|
+
"FINDING_SCHEMA",
|
|
17
|
+
"BASE_INPUT_SCHEMA",
|
|
18
|
+
"validate_findings",
|
|
19
|
+
"generate_agent_payload",
|
|
20
|
+
]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Agent — the SDK's public agent-authoring base class.
|
|
2
|
+
|
|
3
|
+
Provides the shape a native agent needs (metadata class attributes,
|
|
4
|
+
`add_finding`/`set_output`/`effective_config`/`progress`/`fail`, an
|
|
5
|
+
abstract `run()`) while keeping every LLM/tool/file call routed through a
|
|
6
|
+
`Host` (`tenaxisos_sdk.host`) supplied at construction time, never through
|
|
7
|
+
a direct kernel import. In production the kernel supplies a `Host`
|
|
8
|
+
implementation that binds this same class to real governance; for local
|
|
9
|
+
development, `tenaxisos_sdk.testing.local_host.LocalMockHost` is a
|
|
10
|
+
deterministic fake, so an author can write and run tests with no kernel
|
|
11
|
+
installed.
|
|
12
|
+
|
|
13
|
+
What this deliberately does NOT include, since these are the kernel's job,
|
|
14
|
+
not something an agent author should reach for directly:
|
|
15
|
+
- No org/tenant injection, no direct tool registry access — the host
|
|
16
|
+
decides how a tool call actually executes.
|
|
17
|
+
- No LLM provider/task-hint routing — the host's `llm_complete` already
|
|
18
|
+
returns a routed, governed completion.
|
|
19
|
+
- No integration wire-name resolution — an agent calls `self.tools.call(name,
|
|
20
|
+
args)` by logical name; the host resolves the underlying wiring.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
26
|
+
|
|
27
|
+
from tenaxisos_sdk.agents.context import RunContext
|
|
28
|
+
from tenaxisos_sdk.agents.findings import BASE_INPUT_SCHEMA, Finding
|
|
29
|
+
from tenaxisos_sdk.host import Host, ToolCallResult
|
|
30
|
+
|
|
31
|
+
ProgressCallback = Callable[[int, str], Awaitable[None]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Agent(ABC):
|
|
35
|
+
"""Base class for all SDK-authored agents.
|
|
36
|
+
|
|
37
|
+
Each agent implements `run()` and returns a list of `Finding` dicts via
|
|
38
|
+
`self.findings` / `self.add_finding(...)`.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# -- Metadata (override in subclass) ----------------------------------
|
|
42
|
+
display_name: str = "Unknown Agent"
|
|
43
|
+
description: str = ""
|
|
44
|
+
category: str = "general"
|
|
45
|
+
tools: list[str] = []
|
|
46
|
+
capability_tags: list[str] = []
|
|
47
|
+
toolchains: list[str] = []
|
|
48
|
+
input_types: list[str] = []
|
|
49
|
+
output_types: list[str] = []
|
|
50
|
+
CONFIG_SCHEMA: dict = {}
|
|
51
|
+
INPUT_SCHEMA: dict = {}
|
|
52
|
+
OUTPUT_SCHEMA: dict = {}
|
|
53
|
+
DECLARED_OUTPUT_SHAPE: dict = {}
|
|
54
|
+
privacy_tier: str = "standard"
|
|
55
|
+
# -- Behaviour contract ------------------------------------------------
|
|
56
|
+
version: str = "1.0.0"
|
|
57
|
+
deterministic: bool = True
|
|
58
|
+
idempotent: bool = True
|
|
59
|
+
side_effects: str = "none" # none | external | irreversible
|
|
60
|
+
_skip_schema_validation: bool = False
|
|
61
|
+
|
|
62
|
+
def __init__(self, config: dict | None = None, *, host: Host) -> None:
|
|
63
|
+
"""`host` is required and keyword-only: an `Agent` never has an
|
|
64
|
+
implicit default authority channel. Callers must be explicit about
|
|
65
|
+
whether they're binding to `LocalMockHost` or a production host."""
|
|
66
|
+
self.config: dict[str, Any] = config or {}
|
|
67
|
+
self._host = host
|
|
68
|
+
self.findings: list[Finding] = []
|
|
69
|
+
self.output: dict[str, Any] = {}
|
|
70
|
+
self.log_lines: list[str] = []
|
|
71
|
+
self._run_failed = False
|
|
72
|
+
self._fail_reason = ""
|
|
73
|
+
self.ctx = RunContext(self.config.get("context", {}))
|
|
74
|
+
self._validate_config_schema()
|
|
75
|
+
|
|
76
|
+
# -- Contract validation -------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def workflow_variables(self) -> dict[str, Any]:
|
|
79
|
+
"""Flat workflow/run variable values resolved for this run."""
|
|
80
|
+
workflow = self.config.get("workflow", {})
|
|
81
|
+
if isinstance(workflow, dict):
|
|
82
|
+
variables = workflow.get("effective_variables", {})
|
|
83
|
+
if isinstance(variables, dict):
|
|
84
|
+
return variables
|
|
85
|
+
return {}
|
|
86
|
+
|
|
87
|
+
def workflow_var(self, name: str, default: Any = None) -> Any:
|
|
88
|
+
"""Read a resolved workflow variable by name."""
|
|
89
|
+
return self.workflow_variables().get(name, default)
|
|
90
|
+
|
|
91
|
+
def effective_config(self, name: str | None = None, default: Any = None) -> Any:
|
|
92
|
+
"""Read the node's effective runtime config.
|
|
93
|
+
|
|
94
|
+
Effective config is derived by the host using this priority:
|
|
95
|
+
agent defaults < workflow variables < run variables < node config.
|
|
96
|
+
"""
|
|
97
|
+
effective = self.config.get("effective_config")
|
|
98
|
+
if not isinstance(effective, dict):
|
|
99
|
+
effective = self.config
|
|
100
|
+
if name is None:
|
|
101
|
+
return effective
|
|
102
|
+
return effective.get(name, default)
|
|
103
|
+
|
|
104
|
+
def _validate_config_schema(self) -> None:
|
|
105
|
+
if self.__class__._skip_schema_validation:
|
|
106
|
+
return
|
|
107
|
+
schema = self.__class__.CONFIG_SCHEMA or {}
|
|
108
|
+
missing = [k for k, v in schema.items() if v.get("required") and k not in self.config]
|
|
109
|
+
if missing:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"{self.__class__.__name__}: missing required config fields: {', '.join(missing)}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def validate_input(self, runtime_data: dict) -> list[str]:
|
|
115
|
+
"""Validate runtime execution data against INPUT_SCHEMA. Returns a
|
|
116
|
+
list of error strings — empty means valid."""
|
|
117
|
+
errors: list[str] = []
|
|
118
|
+
schema = {**BASE_INPUT_SCHEMA, **self.__class__.INPUT_SCHEMA}
|
|
119
|
+
for field_name, spec in schema.items():
|
|
120
|
+
if spec.get("required") and field_name not in runtime_data:
|
|
121
|
+
errors.append(f"missing required input field: '{field_name}'")
|
|
122
|
+
if field_name in runtime_data:
|
|
123
|
+
allowed = spec.get("enum")
|
|
124
|
+
val = runtime_data[field_name]
|
|
125
|
+
if allowed is not None and val not in allowed:
|
|
126
|
+
errors.append(
|
|
127
|
+
f"input field '{field_name}' must be one of {allowed}, got {val!r}"
|
|
128
|
+
)
|
|
129
|
+
return errors
|
|
130
|
+
|
|
131
|
+
def validate_output(self, findings: list) -> list[str]:
|
|
132
|
+
"""Validate findings against `FINDING_SCHEMA` before they're stored.
|
|
133
|
+
Returns a list of warning strings — empty means valid. Non-fatal:
|
|
134
|
+
callers should append error findings rather than aborting the run."""
|
|
135
|
+
from tenaxisos_sdk.agents.findings import validate_findings
|
|
136
|
+
|
|
137
|
+
return validate_findings(findings)
|
|
138
|
+
|
|
139
|
+
# -- Host-brokered channels ----------------------------------------------
|
|
140
|
+
|
|
141
|
+
async def call_tool(
|
|
142
|
+
self, name: str, args: Optional[dict[str, Any]] = None, **kwargs: Any
|
|
143
|
+
) -> ToolCallResult:
|
|
144
|
+
"""Propose a tool call through the host. The host decides
|
|
145
|
+
allow/deny/approval/dry-run — this never executes anything itself."""
|
|
146
|
+
payload: dict[str, Any] = dict(args or {})
|
|
147
|
+
payload.update(kwargs)
|
|
148
|
+
result = await self._host.call_tool(name, payload)
|
|
149
|
+
self.log_lines.append(f"[tool] {name}: {result.outcome.value}")
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
async def llm_complete(
|
|
153
|
+
self, prompt: str, system: str | None = None, category: str = "llm"
|
|
154
|
+
) -> str | None:
|
|
155
|
+
"""Complete a prompt through the host's governed LLM channel.
|
|
156
|
+
|
|
157
|
+
Mirrors `BaseAgent.llm_complete`: emits an 'error' severity finding
|
|
158
|
+
on failure instead of raising, and returns None so callers can check
|
|
159
|
+
the result without a try/except.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
return await self._host.llm_complete(prompt, system)
|
|
163
|
+
except Exception as exc: # noqa: BLE001 - host failures are reported as findings, not raised
|
|
164
|
+
self.add_finding(
|
|
165
|
+
severity="error",
|
|
166
|
+
category=category,
|
|
167
|
+
title="LLM call failed",
|
|
168
|
+
message=str(exc),
|
|
169
|
+
)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
async def read_file(self, path: str) -> str:
|
|
173
|
+
return await self._host.read_file(path)
|
|
174
|
+
|
|
175
|
+
async def read_file_bytes(self, path: str) -> bytes:
|
|
176
|
+
return await self._host.read_file_bytes(path)
|
|
177
|
+
|
|
178
|
+
async def list_files(self, path: str, pattern: str = "*") -> list[str]:
|
|
179
|
+
return await self._host.list_files(path, pattern)
|
|
180
|
+
|
|
181
|
+
# -- Run lifecycle --------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
async def run(self, on_progress: ProgressCallback | None = None) -> list[dict]:
|
|
185
|
+
"""Execute the agent. Should call `self.progress(...)` periodically.
|
|
186
|
+
Returns findings as dicts (use `self.findings_as_dicts()`)."""
|
|
187
|
+
|
|
188
|
+
async def progress(self, on_progress: Optional[ProgressCallback], pct: int, msg: str) -> None:
|
|
189
|
+
self.log_lines.append(f"[{pct}%] {msg}")
|
|
190
|
+
if on_progress:
|
|
191
|
+
await on_progress(pct, msg)
|
|
192
|
+
|
|
193
|
+
def fail(self, reason: str) -> None:
|
|
194
|
+
"""Mark this run as failed without raising — the caller records
|
|
195
|
+
status='error' even though `run()` returned normally."""
|
|
196
|
+
self._run_failed = True
|
|
197
|
+
self._fail_reason = reason
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def failed(self) -> bool:
|
|
201
|
+
return self._run_failed
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def fail_reason(self) -> str:
|
|
205
|
+
return self._fail_reason
|
|
206
|
+
|
|
207
|
+
# -- Findings / outputs ----------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def add_finding(
|
|
210
|
+
self,
|
|
211
|
+
severity: str,
|
|
212
|
+
category: str,
|
|
213
|
+
title: str,
|
|
214
|
+
message: str,
|
|
215
|
+
file: str | None = None,
|
|
216
|
+
line: int | None = None,
|
|
217
|
+
rule: str | None = None,
|
|
218
|
+
suggestion: str | None = None,
|
|
219
|
+
**extra: Any,
|
|
220
|
+
) -> Finding:
|
|
221
|
+
f = Finding(severity, category, title, message, file, line, rule, suggestion)
|
|
222
|
+
f.extra.update({k: v for k, v in extra.items() if v is not None})
|
|
223
|
+
self.findings.append(f)
|
|
224
|
+
return f
|
|
225
|
+
|
|
226
|
+
def findings_as_dicts(self) -> list[dict]:
|
|
227
|
+
return [f.to_dict() for f in self.findings]
|
|
228
|
+
|
|
229
|
+
def set_output(self, name: str, value: Any) -> None:
|
|
230
|
+
"""Record a declared contract output for this run. The key must be
|
|
231
|
+
declared in `output_types`."""
|
|
232
|
+
if name == "findings" or name not in self.__class__.output_types:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"{self.__class__.__name__}: output '{name}' is not declared in output_types"
|
|
235
|
+
)
|
|
236
|
+
self.output[name] = value
|
|
237
|
+
|
|
238
|
+
def output_as_dict(self) -> dict[str, Any]:
|
|
239
|
+
return dict(self.output)
|
|
240
|
+
|
|
241
|
+
def summary(self) -> dict:
|
|
242
|
+
counts: dict[str, int] = {}
|
|
243
|
+
for f in self.findings:
|
|
244
|
+
counts[f.severity] = counts.get(f.severity, 0) + 1
|
|
245
|
+
return {"total": len(self.findings), "by_severity": counts}
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def docs_metadata(cls) -> dict:
|
|
249
|
+
"""Structured documentation metadata. See `agents/manifest.py` for
|
|
250
|
+
full package-manifest generation."""
|
|
251
|
+
config_docs = {}
|
|
252
|
+
for k, v in (cls.CONFIG_SCHEMA or {}).items():
|
|
253
|
+
config_docs[k] = {
|
|
254
|
+
"type": v.get("type", "str"),
|
|
255
|
+
"required": v.get("required", False),
|
|
256
|
+
"description": v.get("desc", v.get("description", "")),
|
|
257
|
+
"default": v.get("default"),
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
"name": cls.display_name,
|
|
261
|
+
"category": cls.category,
|
|
262
|
+
"description": cls.description,
|
|
263
|
+
"input_types": list(cls.input_types),
|
|
264
|
+
"output_types": list(cls.output_types),
|
|
265
|
+
"config": config_docs,
|
|
266
|
+
"tools": list(cls.tools),
|
|
267
|
+
"capability_tags": list(cls.capability_tags),
|
|
268
|
+
"toolchains": list(cls.toolchains),
|
|
269
|
+
"privacy_tier": cls.privacy_tier,
|
|
270
|
+
"version": cls.version,
|
|
271
|
+
"deterministic": cls.deterministic,
|
|
272
|
+
"idempotent": cls.idempotent,
|
|
273
|
+
"side_effects": cls.side_effects,
|
|
274
|
+
"input_schema": dict(cls.INPUT_SCHEMA),
|
|
275
|
+
"output_schema": dict(cls.OUTPUT_SCHEMA),
|
|
276
|
+
"declared_output_shape": dict(cls.DECLARED_OUTPUT_SHAPE),
|
|
277
|
+
}
|