NOTICE: All information contained herein is, and remains
the property of TechnoCore Automate.
Updated : 2026-03-16
ObjServicePython executes external Python scripts configured in
def_service. The script path is built from the service's
ExternalFolder (or a default directory) plus PayloadTemplate
(the script filename). Parameters are passed as safe command-line
arguments via subprocess.run().
All scripts run inside a 4-layer sandbox that prevents directory
traversal, limits CPU/memory, isolates the working directory, and
strips sensitive environment variables.
This service is the standard mechanism for running custom Python
scripts from within Axion workflows.
Workflow node (type=SERVICE, ServiceCode=MY_SCRIPT)
|
ObjServicePython.execute_script("MY_SCRIPT", p1, p2, p3)
|
1. Load config from def_service WHERE ServiceCode = 'MY_SCRIPT'
2. Build path: ExternalFolder + PayloadTemplate
3. Validate path is within allowed directories
4. Run in sandbox:
- subprocess.run([python, -I, path, p1, p2, p3])
- cwd = temp directory (isolated)
- env = scrubbed (no secrets)
- preexec_fn = CPU + memory limits (POSIX)
- timeout = configurable
|
5. Capture stdout
6. Extract RESULT payload (if present)
7. Return output to workflow context
All scripts execute inside a 4-layer sandbox. Each layer is
configurable via ObjServicePython.yaml.
Scripts must resolve to a path within one of the configured
allowed_dirs. Symlinks and relative paths are resolved via
os.path.realpath() before checking, preventing directory
traversal attacks (../../etc/passwd).
On Linux/macOS, the subprocess runs with CPU and memory limits
enforced via resource.setrlimit(). Prevents CPU bombs and
memory exhaustion.
| Limit | Default | YAML Key |
|---|---|---|
| CPU time | 30 seconds | cpu_limit_seconds |
| Virtual memory | 512 MB | memory_limit_bytes |
Skipped on Windows (os.name == "nt").
The subprocess runs with cwd set to a freshly created temporary
directory. The script cannot read project files, config, or
source code via relative paths. The temp directory is automatically
cleaned up after execution.
Only explicitly allowed environment variables are passed to the
subprocess. All others (including DB passwords, API keys, tokens,
and secrets from config.yaml) are stripped.
| Passed | Stripped |
|---|---|
| PATH, LANG, LC_ALL, PYTHONPATH, VIRTUAL_ENV | Everything else |
HOME is set to the system temp directory (/tmp).
Scripts run with the -I flag (Python isolated mode), which
disables user site-packages directory and ignores PYTHON*
environment variables for import paths.
All sandbox parameters are configurable:
settings:
cpu_limit_seconds: 30
memory_limit_bytes: 512000000
timeout_seconds: 120
allowed_dirs:
- service/script
- factory.service
- resource.test
- resource.bin
safe_env_keys:
- PATH
- LANG
- LC_ALL
- PYTHONPATH
- VIRTUAL_ENV
| Setting | Default | Description |
|---|---|---|
cpu_limit_seconds |
30 | Max CPU time per script (POSIX) |
memory_limit_bytes |
512000000 | Max virtual memory (512 MB) |
timeout_seconds |
120 | Wall-clock timeout |
allowed_dirs |
see above | Directories scripts may run from (relative to project root) |
safe_env_keys |
see above | Environment variables passed to subprocess |
Scripts are configured in the def_service database table:
| Column | Description |
|---|---|
ServiceCode |
Unique identifier for this service config |
ExternalFolder |
Directory containing the script (supports $hostdir$ placeholder) |
PayloadTemplate |
Script filename (e.g. test_service.py) |
RemoteServiceConnection |
Optional remote connection name for credentials |
BuildStructure |
If "Y", validates RESULT payload as JSON |
Default script directory if ExternalFolder is empty:
$hostdir$/service/script/
Scripts communicate structured data back to the workflow by printing
a line containing the RESULT marker followed by a JSON payload:
Normal output lines...
RESULT{"status": "ok", "count": 42, "data": [...]}
The service extracts everything after RESULT and makes it
available as _python_payload in the workflow context. If
BuildStructure is "Y" in def_service, the payload is
validated as JSON.
resource.test/scripts/test_service.py:
"""Test script for ObjServicePython execution."""
import json
import platform
import sys
from datetime import datetime
def main():
params = sys.argv[1:]
result = {
"status": "ok",
"host": platform.node(),
"python": platform.python_version(),
"timestamp": datetime.now().isoformat(),
"params": params,
"param_count": len(params),
}
print(f"Test script running on {result['host']}")
print(f"Parameters: {params}")
print(f"RESULT{json.dumps(result)}")
if __name__ == "__main__":
main()
Sample output:
$ python resource.test/scripts/test_service.py axion test run
Test script running on Hyperion
Parameters: ['axion', 'test', 'run']
RESULT{"status": "ok", "host": "Hyperion", "python": "3.12.3",
"timestamp": "2026-03-16T14:59:24.102863",
"params": ["axion", "test", "run"], "param_count": 3}
Main entry point. Loads service config from def_service, validates
the script path, and runs it inside the sandbox. Returns the full
stdout output.
Replaces placeholders: $id$, $param1$, $param2$, $param3$,
plus standard ObjData placeholders ($hostdir$, $package$, etc.).
Workflow entry point.
| Command | Context Keys | Result Keys |
|---|---|---|
execute |
service_code, param1, param2, param3 |
_python_result (full output), _python_command (script path), _python_payload (RESULT JSON) |
Legacy workflow interfaces preserved for backward compatibility.
| Method | Description |
|---|---|
_read_connection(service_code) |
Load config from def_service via sql_read_object |
_load_remote_credentials(remote) |
Load URL/user/password from Def_RemoteConnections |
_build_command() |
Build script path from ExternalFolder + PayloadTemplate |
_validate_script_path(path) |
Check script is within allowed directories |
_build_clean_env() |
Build scrubbed environment dict |
_make_resource_limiter() |
Return preexec_fn closure with CPU/memory limits |
_parse_result_payload() |
Extract text after RESULT marker from stdout |
# Execute a script in sandbox (validates path)
python ObjServicePython.py execute \
resource.test/scripts/test_service.py hello world
# Execute with all three parameters
python ObjServicePython.py execute \
resource.test/scripts/test_service.py a b c
| Scenario | Behaviour |
|---|---|
No PayloadTemplate configured |
Returns "No script configured in PayloadTemplate" |
| Script outside allowed dirs | Returns "ERROR: script path not allowed" |
| Script not found | subprocess FileNotFoundError caught and logged |
| Script exceeds CPU limit | Killed by OS, caught as non-zero exit |
| Script exceeds memory limit | Killed by OS (OOM), caught as non-zero exit |
| Script timeout (wall-clock) | subprocess.TimeoutExpired caught |
| Script exits non-zero | Output captured, stderr logged as warning |
BuildStructure=Y but invalid JSON |
Warning logged, payload returned as-is |
Host: Hyperion
Script: resource.test/scripts/test_service.py
Sandbox: 4 layers active (path, limits, tmpdir, env)
CPU: 30s limit
Memory: 512MB limit
Timeout: 120s
Env keys: HOME=/tmp, PATH, LANG, VIRTUAL_ENV
Result: {"status": "ok", "host": "Hyperion", ...}
Path validation:
resource.test/scripts/test_service.py — ALLOWED
resource.bin/start_mariadb.sh — ALLOWED
factory.service/package.core/... — ALLOWED
/etc/passwd — BLOCKED
../../config.yaml — BLOCKED
from ObjServicePython import ObjServiceApi
svc = ObjServiceApi()
# Via service config (reads def_service table)
output = svc.execute_script(
"MY_SCRIPT", param1="input.csv"
)
print(output)
# Via workflow context
context = {
"command": "execute",
"service_code": "MY_SCRIPT",
"param1": "input.csv",
}
svc.process(context)
print(context["_python_result"])
print(context["_python_payload"])
Updated : 2026-03-16
cythonize -3 -a -i ObjServicePython.py
Compiling /home/axion/projects/axion/factory.service/package.core/ObjServicePython.py because it changed..[1/1] Cythonizing /home/axion/projects/axion/factory.service/package.core/ObjServicePython.py
Updated : 2026-03-16