peteos 0.0.2__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.
- peteos-0.0.2/LICENSE +60 -0
- peteos-0.0.2/MANIFEST.in +2 -0
- peteos-0.0.2/PKG-INFO +22 -0
- peteos-0.0.2/README.md +112 -0
- peteos-0.0.2/peteos/__init__.py +28 -0
- peteos-0.0.2/peteos/agentic_objects/__init__.py +20 -0
- peteos-0.0.2/peteos/agentic_objects/bash_workspace.py +219 -0
- peteos-0.0.2/peteos/agentic_objects/camera_driver/__init__.py +8 -0
- peteos-0.0.2/peteos/agentic_objects/camera_driver/camera_driver.py +50 -0
- peteos-0.0.2/peteos/agentic_objects/camera_driver/cv2_camera_driver.py +53 -0
- peteos-0.0.2/peteos/agentic_objects/camera_observer.py +131 -0
- peteos-0.0.2/peteos/agentic_objects/multi_camera_observer.py +133 -0
- peteos-0.0.2/peteos/agentic_objects/pdf_transcriber.py +202 -0
- peteos-0.0.2/peteos/agentic_objects/string_comparator.py +59 -0
- peteos-0.0.2/peteos/agentic_objects/text_editor.py +156 -0
- peteos-0.0.2/peteos/agentic_objects/web_navigator.py +302 -0
- peteos-0.0.2/peteos/channels/__init__.py +6 -0
- peteos-0.0.2/peteos/channels/nextcloud_talk_channel.py +635 -0
- peteos-0.0.2/peteos/channels/shell_channel.py +246 -0
- peteos-0.0.2/peteos/channels/stdout_channel.py +236 -0
- peteos-0.0.2/peteos/chatbot/__init__.py +20 -0
- peteos-0.0.2/peteos/chatbot/anthropicchatbot.py +427 -0
- peteos-0.0.2/peteos/chatbot/anthropicprovider.py +28 -0
- peteos-0.0.2/peteos/chatbot/backendconfig.py +82 -0
- peteos-0.0.2/peteos/chatbot/backendprovider.py +45 -0
- peteos-0.0.2/peteos/chatbot/chatbot.py +180 -0
- peteos-0.0.2/peteos/chatbot/chatbotconfig.py +59 -0
- peteos-0.0.2/peteos/chatbot/chatbotresponse.py +226 -0
- peteos-0.0.2/peteos/chatbot/geminichatbot.py +651 -0
- peteos-0.0.2/peteos/chatbot/geminiprovider.py +48 -0
- peteos-0.0.2/peteos/chatbot/httpclient.py +152 -0
- peteos-0.0.2/peteos/chatbot/manager.py +344 -0
- peteos-0.0.2/peteos/chatbot/openaichatbot.py +576 -0
- peteos-0.0.2/peteos/chatbot/openaiprovider.py +28 -0
- peteos-0.0.2/peteos/chatbot/response_types.py +38 -0
- peteos-0.0.2/peteos/chatbot/simplemock.py +101 -0
- peteos-0.0.2/peteos/config.py +136 -0
- peteos-0.0.2/peteos/conversation/__init__.py +27 -0
- peteos-0.0.2/peteos/conversation/context.py +660 -0
- peteos-0.0.2/peteos/conversation/media.py +256 -0
- peteos-0.0.2/peteos/conversation/message.py +357 -0
- peteos-0.0.2/peteos/conversation/message_registry.py +61 -0
- peteos-0.0.2/peteos/conversation/session.py +348 -0
- peteos-0.0.2/peteos/conversation/system_prompt_message.py +81 -0
- peteos-0.0.2/peteos/conversation/tool_definitions_message.py +167 -0
- peteos-0.0.2/peteos/engine/__init__.py +39 -0
- peteos-0.0.2/peteos/engine/channel.py +143 -0
- peteos-0.0.2/peteos/engine/exec_status.py +40 -0
- peteos-0.0.2/peteos/engine/executionenvironment.py +468 -0
- peteos-0.0.2/peteos/engine/runner.py +601 -0
- peteos-0.0.2/peteos/oap/__init__.py +14 -0
- peteos-0.0.2/peteos/oap/adaptive_object.py +292 -0
- peteos-0.0.2/peteos/oap/agentic_object.py +813 -0
- peteos-0.0.2/peteos/oap/agentic_registry.py +204 -0
- peteos-0.0.2/peteos/oap/decorators.py +68 -0
- peteos-0.0.2/peteos/oap/error.py +19 -0
- peteos-0.0.2/peteos/oap/prompts.py +33 -0
- peteos-0.0.2/peteos/oap/token_counter.py +69 -0
- peteos-0.0.2/peteos/persona/__init__.py +8 -0
- peteos-0.0.2/peteos/persona/agent.py +168 -0
- peteos-0.0.2/peteos/persona/role.py +206 -0
- peteos-0.0.2/peteos/persona/rolemanager.py +80 -0
- peteos-0.0.2/peteos/persona/toolmanager.py +181 -0
- peteos-0.0.2/peteos/sandbox/__init__.py +21 -0
- peteos-0.0.2/peteos/sandbox/sandbox.py +36 -0
- peteos-0.0.2/peteos/sandbox/sandbox_builder.py +686 -0
- peteos-0.0.2/peteos/sandbox/scope.py +19 -0
- peteos-0.0.2/peteos/utils/__init__.py +6 -0
- peteos-0.0.2/peteos/utils/_schema.py +497 -0
- peteos-0.0.2/peteos/utils/activeclass.py +154 -0
- peteos-0.0.2/peteos/utils/delta_merge.py +235 -0
- peteos-0.0.2/peteos/utils/dict_path.py +171 -0
- peteos-0.0.2/peteos/utils/json.py +42 -0
- peteos-0.0.2/peteos/utils/logger.py +89 -0
- peteos-0.0.2/peteos/utils/tiktoken.py +26 -0
- peteos-0.0.2/peteos.egg-info/PKG-INFO +22 -0
- peteos-0.0.2/peteos.egg-info/SOURCES.txt +82 -0
- peteos-0.0.2/peteos.egg-info/dependency_links.txt +1 -0
- peteos-0.0.2/peteos.egg-info/not-zip-safe +1 -0
- peteos-0.0.2/peteos.egg-info/requires.txt +18 -0
- peteos-0.0.2/peteos.egg-info/top_level.txt +1 -0
- peteos-0.0.2/pyproject.toml +27 -0
- peteos-0.0.2/setup.cfg +4 -0
- peteos-0.0.2/setup.py +14 -0
peteos-0.0.2/LICENSE
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
-------
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Schäfer List Systems GmbH
|
|
5
|
+
|
|
6
|
+
This software is dual-licensed.
|
|
7
|
+
|
|
8
|
+
1. NON-COMMERCIAL LICENSE (FREE)
|
|
9
|
+
|
|
10
|
+
1.1 Grant of License
|
|
11
|
+
Subject to the terms of this Section 1, you are granted a non-exclusive,
|
|
12
|
+
non-transferable, worldwide, royalty-free license to use, copy, modify,
|
|
13
|
+
and distribute this software, solely for non-commercial purposes.
|
|
14
|
+
|
|
15
|
+
1.2 Definition of Non-Commercial Use
|
|
16
|
+
"Non-commercial use" means any use that is not primarily intended for or
|
|
17
|
+
directed towards commercial advantage or monetary compensation, including
|
|
18
|
+
but not limited to:
|
|
19
|
+
- personal projects,
|
|
20
|
+
- academic research,
|
|
21
|
+
- internal evaluation and testing,
|
|
22
|
+
- evaluation of potential commercial use cases within your organization.
|
|
23
|
+
|
|
24
|
+
1.3 Evaluation of Commercial Use Cases
|
|
25
|
+
You may use this software under this non-commercial license to evaluate
|
|
26
|
+
its suitability for a commercial use case within your organization.
|
|
27
|
+
However, any actual deployment, production use, or integration into a
|
|
28
|
+
product or service that is offered commercially requires a separate
|
|
29
|
+
commercial license from Your Company.
|
|
30
|
+
|
|
31
|
+
1.4 Restrictions
|
|
32
|
+
Under this non-commercial license you may NOT:
|
|
33
|
+
- use the software in any product or service offered for sale, license,
|
|
34
|
+
or subscription;
|
|
35
|
+
- use the software to provide any commercial hosting, SaaS, or managed
|
|
36
|
+
service;
|
|
37
|
+
- remove or alter copyright notices or license texts.
|
|
38
|
+
|
|
39
|
+
1.5 No Warranty
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
41
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
42
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
43
|
+
|
|
44
|
+
2. COMMERCIAL LICENSE
|
|
45
|
+
|
|
46
|
+
If you wish to use this software for any commercial purpose, including but
|
|
47
|
+
not limited to production deployment, integration into a commercial product,
|
|
48
|
+
or offering any service based on this software, you must obtain a commercial
|
|
49
|
+
license from Your Company.
|
|
50
|
+
|
|
51
|
+
For commercial licensing, please contact:
|
|
52
|
+
Email: info@schaeferlist.de
|
|
53
|
+
Web: https://www.schaeferlist.de
|
|
54
|
+
|
|
55
|
+
3. GENERAL
|
|
56
|
+
|
|
57
|
+
This dual-licensing model is permissible because the Schaefer List Systems GmbH
|
|
58
|
+
owns all copyrights in this software. By using this software, you agree to comply
|
|
59
|
+
with either the non-commercial license (Section 1) or a valid commercial
|
|
60
|
+
license (Section 2), as applicable to your use case.
|
peteos-0.0.2/MANIFEST.in
ADDED
peteos-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: peteos
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Agentic application framework
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: json5>=0.15.0
|
|
8
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: tiktoken>=0.7.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
14
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
15
|
+
Requires-Dist: cython>=3.0; extra == "dev"
|
|
16
|
+
Provides-Extra: camera
|
|
17
|
+
Requires-Dist: opencv-python>=4.8.0; extra == "camera"
|
|
18
|
+
Requires-Dist: numpy>=1.24.0; extra == "camera"
|
|
19
|
+
Provides-Extra: web
|
|
20
|
+
Requires-Dist: beautifulsoup4; extra == "web"
|
|
21
|
+
Requires-Dist: lxml; extra == "web"
|
|
22
|
+
Dynamic: license-file
|
peteos-0.0.2/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Peteos
|
|
2
|
+
|
|
3
|
+
Simple Object-Agentic Programming (sOAP) — an agentic application framework that brings together object-oriented programming and AI agents.
|
|
4
|
+
The easiest way to build reliable and powerful multi-agent systems.
|
|
5
|
+
|
|
6
|
+
Peteos lets you build **agentic objects**: ordinary Python objects that can think, decide, and act on their own.
|
|
7
|
+
No external memory stores or bolt-on intelligence — the object itself is the center of persistence and agency.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
After installing and configuring Peteos, you can build and use agentic objects like the following.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
class Example(AgenticObject):
|
|
15
|
+
"""You are Pete, a concise assistant."""
|
|
16
|
+
def __init__(self, job: str):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self._job = job
|
|
19
|
+
|
|
20
|
+
@tool
|
|
21
|
+
def get_job(self) -> str:
|
|
22
|
+
return self._job
|
|
23
|
+
|
|
24
|
+
pete = Example("demonstrator")
|
|
25
|
+
result = await pete.invoke_agent("Hello, what's your name and job?", output_schema=list[str])
|
|
26
|
+
print(result) # ['Pete', 'demonstrator']
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Key Features
|
|
30
|
+
|
|
31
|
+
The documentation and the examples cover more complex agentic objects with the following key features.
|
|
32
|
+
|
|
33
|
+
- **Agentic objects** — derive from `AgenticObject` and get a thinking agent behind every instance.
|
|
34
|
+
- **Inheritance** — inherit from agentic objects to refine the system prompt and extend its toolset.
|
|
35
|
+
- **Multi-inheritance composition** — combine agentic classes to compose behavior.
|
|
36
|
+
- **Structured output** — declare `output_schema` and get typed results back.
|
|
37
|
+
- **Sandboxed code execution** — let agents run Python in a restricted sandbox.
|
|
38
|
+
- **Persistent sessions** — threads, memory, and sub-agent coordination via `persistent_thread_id`.
|
|
39
|
+
- **Test-driven development** — it's OOP, so test your agents as you test any other software.
|
|
40
|
+
- **Debuggable agents** — it's plain Python, so debug your agents with a standard debugger.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
### From source
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone --recurse-submodules https://github.com/yourusername/peteos.git
|
|
49
|
+
cd peteos
|
|
50
|
+
python3 -m venv .venv
|
|
51
|
+
source .venv/bin/activate
|
|
52
|
+
pip install -e ".[dev]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### From a distribution package
|
|
56
|
+
|
|
57
|
+
Download a `.tar.gz` archive and extract it, then install the wheel inside:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
tar xzf peteos-0.0.1.tar.gz
|
|
61
|
+
cd peteos-0.0.1
|
|
62
|
+
python -m venv .venv
|
|
63
|
+
source .venv/bin/activate
|
|
64
|
+
pip install peteos-0.0.1-cp312-cp312-linux_x86_64.whl
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### From a Python package (PyPI / direct install)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m venv .venv
|
|
71
|
+
source .venv/bin/activate
|
|
72
|
+
pip install peteos
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Copy `peteos.json.example` to `peteos.json` and adapt it to your environment:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cp peteos.json.example peteos.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The configuration file defines chatbot backends (LLM providers) that Peteos uses for agent reasoning:
|
|
84
|
+
|
|
85
|
+
- **name** — a descriptive label for the backend
|
|
86
|
+
- **url** — the API endpoint for the provider
|
|
87
|
+
- **api_type** — `anthropic`, `gemini`, or omitted for OpenAI-compatible endpoints
|
|
88
|
+
- **api_key** — environment variable name holding the API key (never hardcode keys)
|
|
89
|
+
- **model_priorities** — maps model names to priority scores (higher = preferred)
|
|
90
|
+
|
|
91
|
+
To verify that your configuration is functional run `python3 examples/00_hello_pete.py`.
|
|
92
|
+
|
|
93
|
+
## Resources
|
|
94
|
+
|
|
95
|
+
- [Getting Started](docs/getting-started.md) — installation and first example
|
|
96
|
+
- [Introduction](docs/introduction.md) — the sOAP paradigm
|
|
97
|
+
- [Concepts](docs/concepts/index.md) — composition, invocation, state, testing
|
|
98
|
+
- [Reference](docs/reference/index.md) — API docs, agentic objects
|
|
99
|
+
- [Best Practices](docs/best-practices/index.md) — Best practices when using PeteOS
|
|
100
|
+
- [Examples](docs/examples/) — Examples show-casing some features
|
|
101
|
+
|
|
102
|
+
## Licensing
|
|
103
|
+
|
|
104
|
+
This project is dual-licensed:
|
|
105
|
+
|
|
106
|
+
- **Non-commercial use** (including evaluation of commercial use cases) is free
|
|
107
|
+
under the license in the `LICENSE` file.
|
|
108
|
+
- **Commercial use** (production, products, services, SaaS, etc.) requires a
|
|
109
|
+
commercial license from the Schäfer List Systems GmbH.
|
|
110
|
+
|
|
111
|
+
If you intend to use this software commercially, please contact us at
|
|
112
|
+
<info@schaeferlist.de> or visit <https://www.schaeferlist.de>.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Peteos - Agentic application framework
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from peteos.config import ConfigManager
|
|
6
|
+
|
|
7
|
+
# OAP primitives
|
|
8
|
+
from peteos.oap import (
|
|
9
|
+
AgenticObject,
|
|
10
|
+
AdaptiveObject,
|
|
11
|
+
Error,
|
|
12
|
+
agentic_object,
|
|
13
|
+
tool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AgenticObject",
|
|
18
|
+
"AdaptiveObject",
|
|
19
|
+
"Error",
|
|
20
|
+
"agentic_object",
|
|
21
|
+
"tool",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Bootstrap backends and roles from peteos.json at import time.
|
|
25
|
+
try:
|
|
26
|
+
asyncio.get_running_loop()
|
|
27
|
+
except RuntimeError:
|
|
28
|
+
asyncio.run(ConfigManager.init())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Agentic Objects - concrete implementations using the OAP framework."""
|
|
2
|
+
|
|
3
|
+
from peteos.agentic_objects.camera_driver import CameraDriver, CV2CameraDriver
|
|
4
|
+
from peteos.agentic_objects.camera_observer import CameraObserver
|
|
5
|
+
from peteos.agentic_objects.multi_camera_observer import MultiCameraObserver
|
|
6
|
+
from peteos.agentic_objects.pdf_transcriber import PdfTranscriber
|
|
7
|
+
from peteos.agentic_objects.bash_workspace import BashWorkspace
|
|
8
|
+
from peteos.agentic_objects.string_comparator import AgenticStringComparator
|
|
9
|
+
from peteos.agentic_objects.text_editor import TextEditor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AgenticStringComparator",
|
|
13
|
+
"BashWorkspace",
|
|
14
|
+
"CameraDriver",
|
|
15
|
+
"CV2CameraDriver",
|
|
16
|
+
"CameraObserver",
|
|
17
|
+
"MultiCameraObserver",
|
|
18
|
+
"PdfTranscriber",
|
|
19
|
+
"TextEditor",
|
|
20
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""BashWorkspace - secure bash execution agentic object."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from peteos.oap.agentic_object import AgenticObject
|
|
12
|
+
from peteos.oap.decorators import tool
|
|
13
|
+
from peteos.utils import get_logger
|
|
14
|
+
|
|
15
|
+
_logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from peteos.engine import Runner
|
|
19
|
+
|
|
20
|
+
# Safe POSIX utilities available in the sandbox.
|
|
21
|
+
# No network tools (curl, wget, nc), no shells (bash -c via env vars), no
|
|
22
|
+
# process control (kill, top), no system introspection (ps, whoami).
|
|
23
|
+
_SAFE_COMMANDS: set[str] = {
|
|
24
|
+
"cat", "echo", "grep", "egrep", "sed", "awk", "sort", "uniq", "wc",
|
|
25
|
+
"head", "tail", "cut", "tr", "mkdir", "cp", "mv", "rm", "ln", "chmod",
|
|
26
|
+
"touch", "find", "basename", "dirname", "test", "date", "sleep",
|
|
27
|
+
"tee", "xargs", "shuf", "paste", "join", "diff", "comm", "uniq",
|
|
28
|
+
"base64", "md5sum", "sha256sum", "stat", "du", "file", "ls",
|
|
29
|
+
# Bash builtins — executed via /bin/sh -c, so not listed here directly.
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Strictly minimal POSIX environment: no PATH leaks, no secrets, no user config.
|
|
33
|
+
_MINIMAL_ENV: dict[str, str] = {
|
|
34
|
+
"PATH": "/usr/bin:/bin",
|
|
35
|
+
"HOME": "/tmp/bash_workspace_home",
|
|
36
|
+
"LANG": "C",
|
|
37
|
+
"LC_ALL": "C",
|
|
38
|
+
"TERM": "dumb",
|
|
39
|
+
"TMPDIR": "/tmp",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BashWorkspace(AgenticObject):
|
|
44
|
+
"""You are a secure bash workspace for executing shell commands.
|
|
45
|
+
|
|
46
|
+
You can run bash commands on files placed in your workspace. You have
|
|
47
|
+
NO access to the rest of the filesystem beyond your workspace directory.
|
|
48
|
+
The environment is minimal: no network tools, no secrets, no user data.
|
|
49
|
+
|
|
50
|
+
Your workspace is created fresh for each invocation and destroyed
|
|
51
|
+
afterward. Files must be provided to you — you cannot access anything
|
|
52
|
+
outside your workspace directory.
|
|
53
|
+
|
|
54
|
+
Available commands: cat, echo, grep, sed, awk, sort, uniq, wc, head,
|
|
55
|
+
tail, cut, tr, mkdir, cp, mv, rm, ln, chmod, touch, find, basename,
|
|
56
|
+
dirname, test, date, sleep, tee, xargs, shuf, paste, join, diff, comm,
|
|
57
|
+
base64, md5sum, sha256sum, stat, du, file, ls.
|
|
58
|
+
|
|
59
|
+
No network tools (curl, wget, nc) or shell escapes are available.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._workspace_dir: Path | None = None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def workspace_dir(self) -> Path | None:
|
|
68
|
+
"""The current workspace directory, or None if not active."""
|
|
69
|
+
return self._workspace_dir
|
|
70
|
+
|
|
71
|
+
def _setup_workspace(self) -> Path:
|
|
72
|
+
"""Create or return the workspace directory.
|
|
73
|
+
|
|
74
|
+
Uses an existing workspace if available, otherwise creates a new
|
|
75
|
+
temporary directory.
|
|
76
|
+
"""
|
|
77
|
+
if self._workspace_dir is None or not self._workspace_dir.is_dir():
|
|
78
|
+
self._workspace_dir = Path(tempfile.mkdtemp(prefix="bash_workspace_"))
|
|
79
|
+
# Create HOME dir so tools like `ssh` (if it slipped through)
|
|
80
|
+
# don't error on missing ~/.ssh — this is a defense-in-depth.
|
|
81
|
+
(self._workspace_dir / "home").mkdir(exist_ok=True)
|
|
82
|
+
os.environ["HOME"] = str(self._workspace_dir / "home")
|
|
83
|
+
return self._workspace_dir
|
|
84
|
+
|
|
85
|
+
def _teardown_workspace(self) -> None:
|
|
86
|
+
"""Remove the workspace directory."""
|
|
87
|
+
if self._workspace_dir and self._workspace_dir.is_dir():
|
|
88
|
+
import shutil
|
|
89
|
+
shutil.rmtree(self._workspace_dir, ignore_errors=True)
|
|
90
|
+
self._workspace_dir = None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def _env(self) -> dict[str, str]:
|
|
94
|
+
"""Build the sandbox environment with the workspace HOME."""
|
|
95
|
+
env = {**_MINIMAL_ENV}
|
|
96
|
+
if self._workspace_dir:
|
|
97
|
+
env["HOME"] = str(self._workspace_dir / "home")
|
|
98
|
+
# Inherit only a few safe env vars from the parent process.
|
|
99
|
+
for key in ("TERM", "LANG", "LC_ALL"):
|
|
100
|
+
val = os.environ.get(key)
|
|
101
|
+
if val:
|
|
102
|
+
env[key] = val
|
|
103
|
+
return env
|
|
104
|
+
|
|
105
|
+
def _is_safe_command(self, command: str) -> bool:
|
|
106
|
+
"""Check if the command (first word) is in the allowlist."""
|
|
107
|
+
first = command.strip().split()[0] if command.strip() else ""
|
|
108
|
+
# Handle built-in /bin/sh -c wrapper
|
|
109
|
+
if first == "/bin/sh" or first == "sh":
|
|
110
|
+
return True
|
|
111
|
+
# Extract basename for /usr/bin/foo paths
|
|
112
|
+
base = os.path.basename(first)
|
|
113
|
+
return base in _SAFE_COMMANDS
|
|
114
|
+
|
|
115
|
+
@tool
|
|
116
|
+
def bash_exec(self, command: str, timeout: int = 30) -> str:
|
|
117
|
+
"""Execute a bash command in the sandboxed workspace.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
command: The bash command to execute.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A string containing exit code, stdout, and stderr information.
|
|
124
|
+
"""
|
|
125
|
+
ws = self._setup_workspace()
|
|
126
|
+
|
|
127
|
+
if not command.strip():
|
|
128
|
+
return "Error: Empty command."
|
|
129
|
+
|
|
130
|
+
# Block path traversal patterns in the raw command text before
|
|
131
|
+
# we even spawn a shell. This catches ../ escape attempts early.
|
|
132
|
+
if ".." in command:
|
|
133
|
+
return "Error: Path traversal is not allowed."
|
|
134
|
+
|
|
135
|
+
# Block command substitution and backticks that could bypass
|
|
136
|
+
# the allowlist via subshell execution.
|
|
137
|
+
_BLOCKED_CHARS = ("`", "$(")
|
|
138
|
+
for bad in _BLOCKED_CHARS:
|
|
139
|
+
if bad in command:
|
|
140
|
+
return f"Error: The character or sequence '{bad}' is not allowed."
|
|
141
|
+
|
|
142
|
+
# Quick allowlist check on the first token to catch obviously
|
|
143
|
+
# dangerous commands before we even spawn a shell.
|
|
144
|
+
if not self._is_safe_command(command):
|
|
145
|
+
base = os.path.basename(command.strip().split()[0])
|
|
146
|
+
return f"Error: Command '{base}' is not allowed in the sandbox."
|
|
147
|
+
|
|
148
|
+
_logger.debug("[bash_exec] ws=%s, command=%r", ws, command)
|
|
149
|
+
try:
|
|
150
|
+
result = subprocess.run(
|
|
151
|
+
["/bin/sh", "-c", command],
|
|
152
|
+
cwd=ws,
|
|
153
|
+
env=self._env,
|
|
154
|
+
capture_output=True,
|
|
155
|
+
text=True,
|
|
156
|
+
timeout=timeout,
|
|
157
|
+
)
|
|
158
|
+
_logger.debug("[bash_exec] stdout=%r, stderr=%r, returncode=%d", result.stdout, result.stderr, result.returncode)
|
|
159
|
+
output = f"exit_code: {result.returncode}"
|
|
160
|
+
if result.stdout:
|
|
161
|
+
output += f"\nstdout:\n{result.stdout}"
|
|
162
|
+
if result.stderr:
|
|
163
|
+
output += f"\nstderr:\n{result.stderr}"
|
|
164
|
+
if not result.stdout and not result.stderr:
|
|
165
|
+
output = "(no output)"
|
|
166
|
+
return output
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
return "Error: Command timed out after 30 seconds."
|
|
169
|
+
except Exception as e:
|
|
170
|
+
return f"Error: {type(e).__name__}: {e}"
|
|
171
|
+
|
|
172
|
+
@tool
|
|
173
|
+
def put_file(self, name: str, content: str) -> str:
|
|
174
|
+
"""Place a file into the workspace.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: The filename to create in the workspace.
|
|
178
|
+
content: The file contents as a string.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Confirmation message.
|
|
182
|
+
"""
|
|
183
|
+
ws = self._setup_workspace()
|
|
184
|
+
filepath = (ws / name).resolve()
|
|
185
|
+
# Guard: prevent directory traversal outside workspace.
|
|
186
|
+
# resolve() normalizes .. so this is a proper containment check.
|
|
187
|
+
try:
|
|
188
|
+
filepath.relative_to(ws.resolve())
|
|
189
|
+
except ValueError:
|
|
190
|
+
return f"Error: File name '{name}' is not allowed."
|
|
191
|
+
try:
|
|
192
|
+
filepath.write_text(content)
|
|
193
|
+
return f"OK: File '{name}' successfully written."
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return f"Error: Failed to write '{name}': {e}"
|
|
196
|
+
|
|
197
|
+
@tool
|
|
198
|
+
def get_file(self, name: str) -> str:
|
|
199
|
+
"""Read the content of a file from the workspace.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
name: The filename to read from the workspace.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The file contents as a string.
|
|
206
|
+
"""
|
|
207
|
+
ws = self._setup_workspace()
|
|
208
|
+
filepath = (ws / name).resolve()
|
|
209
|
+
# resolve() normalizes .. so this is a proper containment check.
|
|
210
|
+
try:
|
|
211
|
+
filepath.relative_to(ws.resolve())
|
|
212
|
+
except ValueError:
|
|
213
|
+
return f"Error: File name '{name}' is not allowed."
|
|
214
|
+
if not filepath.is_file():
|
|
215
|
+
return f"Error: File '{name}' not found in workspace."
|
|
216
|
+
try:
|
|
217
|
+
return filepath.read_text()
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return f"Error: Failed to read '{name}': {e}"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""CameraDriver - abstract interface for camera backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from peteos.agentic_objects.camera_driver.camera_driver import CameraDriver
|
|
6
|
+
from peteos.agentic_objects.camera_driver.cv2_camera_driver import CV2CameraDriver
|
|
7
|
+
|
|
8
|
+
__all__ = ["CameraDriver", "CV2CameraDriver"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""CameraDriver - abstract interface for camera backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CameraDriver(ABC):
|
|
11
|
+
"""Interface for camera backends.
|
|
12
|
+
|
|
13
|
+
A driver is responsible for listing, opening, closing, and grabbing
|
|
14
|
+
frames from a specific camera subsystem (e.g. V4L2, USB, RTSP).
|
|
15
|
+
The driver returns raw numpy arrays; encoding/scaling is handled
|
|
16
|
+
by the observer.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
driver_name: str = ""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def list_cameras(self) -> list[tuple[int, str]]:
|
|
23
|
+
"""List available cameras.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of (camera_id, description) tuples.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def open(self, camera_id: int) -> bool:
|
|
31
|
+
"""Open a camera for capturing.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
camera_id: The camera ID from ``list_cameras``.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if the camera was opened successfully.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def close(self) -> None:
|
|
42
|
+
"""Close the currently open camera."""
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def grab_frame(self) -> tuple[bool, np.ndarray, tuple[int, int]]:
|
|
46
|
+
"""Grab a single raw frame.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (success, numpy array, (width, height)).
|
|
50
|
+
"""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""CV2CameraDriver - portable V4L2 camera driver via OpenCV.
|
|
2
|
+
|
|
3
|
+
Unlike OpenCVCameraDriver, this driver does not shell out to
|
|
4
|
+
v4l2-ctl. It simply tests indices 0-9 with OpenCV, making it
|
|
5
|
+
portable across Linux, Windows, and macOS.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import cv2
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from peteos.agentic_objects.camera_driver import CameraDriver
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CV2CameraDriver(CameraDriver):
|
|
22
|
+
"""Portable V4L2 driver backed by OpenCV only."""
|
|
23
|
+
|
|
24
|
+
driver_name = "opencv-cv2"
|
|
25
|
+
|
|
26
|
+
def list_cameras(self) -> list[tuple[int, str]]:
|
|
27
|
+
cameras: list[tuple[int, str]] = []
|
|
28
|
+
for i in range(10):
|
|
29
|
+
cap = cv2.VideoCapture(i)
|
|
30
|
+
if cap.isOpened():
|
|
31
|
+
cameras.append((i, ""))
|
|
32
|
+
cap.release()
|
|
33
|
+
return cameras
|
|
34
|
+
|
|
35
|
+
def open(self, camera_id: int) -> bool:
|
|
36
|
+
cap = cv2.VideoCapture(camera_id)
|
|
37
|
+
if not cap.isOpened():
|
|
38
|
+
return False
|
|
39
|
+
self._cap = cap
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
if hasattr(self, "_cap"):
|
|
44
|
+
self._cap.release()
|
|
45
|
+
del self._cap
|
|
46
|
+
|
|
47
|
+
def grab_frame(self) -> tuple[bool, np.ndarray, tuple[int, int]]:
|
|
48
|
+
if not hasattr(self, "_cap"):
|
|
49
|
+
return False, np.empty((0, 0, 3)), (0, 0)
|
|
50
|
+
ret, frame = self._cap.read()
|
|
51
|
+
if not ret or frame is None:
|
|
52
|
+
return False, np.empty((0, 0, 3)), (0, 0)
|
|
53
|
+
return True, frame, (frame.shape[1], frame.shape[0])
|