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.
Files changed (54) hide show
  1. tenaxisos_sdk-0.2.0/LICENSE +21 -0
  2. tenaxisos_sdk-0.2.0/PKG-INFO +131 -0
  3. tenaxisos_sdk-0.2.0/README.md +108 -0
  4. tenaxisos_sdk-0.2.0/pyproject.toml +55 -0
  5. tenaxisos_sdk-0.2.0/setup.cfg +4 -0
  6. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/__init__.py +14 -0
  7. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/__init__.py +20 -0
  8. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/base.py +277 -0
  9. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/context.py +132 -0
  10. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/findings.py +96 -0
  11. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/agents/manifest.py +158 -0
  12. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/cli/__init__.py +1 -0
  13. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/cli/main.py +956 -0
  14. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/__init__.py +9 -0
  15. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/manifest.py +194 -0
  16. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schema.py +53 -0
  17. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schemas/manifest.v0.schema.json +198 -0
  18. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/schemas/playbook.v0.schema.json +96 -0
  19. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/core/signing.py +106 -0
  20. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/host/__init__.py +111 -0
  21. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/importers/__init__.py +10 -0
  22. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/importers/n8n.py +313 -0
  23. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/__init__.py +5 -0
  24. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/__init__.py +3 -0
  25. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/base.py +62 -0
  26. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/runtime/adapters/langchain.py +161 -0
  27. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/testing/__init__.py +3 -0
  28. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/testing/local_host.py +90 -0
  29. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/__init__.py +12 -0
  30. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/base.py +66 -0
  31. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/manifest.py +105 -0
  32. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/schema.py +44 -0
  33. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk/tools/stdio.py +57 -0
  34. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/PKG-INFO +131 -0
  35. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/SOURCES.txt +52 -0
  36. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/dependency_links.txt +1 -0
  37. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/entry_points.txt +2 -0
  38. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/requires.txt +16 -0
  39. tenaxisos_sdk-0.2.0/src/tenaxisos_sdk.egg-info/top_level.txt +1 -0
  40. tenaxisos_sdk-0.2.0/tests/test_agents_authoring.py +156 -0
  41. tenaxisos_sdk-0.2.0/tests/test_cli_agent_build.py +70 -0
  42. tenaxisos_sdk-0.2.0/tests/test_cli_mcp.py +107 -0
  43. tenaxisos_sdk-0.2.0/tests/test_cli_playbook.py +70 -0
  44. tenaxisos_sdk-0.2.0/tests/test_cli_publish.py +163 -0
  45. tenaxisos_sdk-0.2.0/tests/test_cli_run.py +89 -0
  46. tenaxisos_sdk-0.2.0/tests/test_cli_verify.py +93 -0
  47. tenaxisos_sdk-0.2.0/tests/test_cli_workflow_import.py +61 -0
  48. tenaxisos_sdk-0.2.0/tests/test_langchain_adapter.py +124 -0
  49. tenaxisos_sdk-0.2.0/tests/test_manifest.py +203 -0
  50. tenaxisos_sdk-0.2.0/tests/test_mcp_tools.py +119 -0
  51. tenaxisos_sdk-0.2.0/tests/test_n8n_importer.py +125 -0
  52. tenaxisos_sdk-0.2.0/tests/test_native_agent_build.py +68 -0
  53. tenaxisos_sdk-0.2.0/tests/test_sdk_compatibility.py +75 -0
  54. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ }