Thin Mode — Wrap an Existing API
Thin mode is the fastest adoption path: keep your existing service as-is, and expose it as an atomic NodeType by running a small “wrapper node” inside Atomic Executor.
This tutorial is intentionally practical: you’ll build a real, runnable node pack and verify it works.
ARP Standard defines the execution contracts (NodeType, NodeRun, Atomic Executor). How you author and distribute node handlers is an implementation choice.
JARVIS (v0.x) uses a Python “node pack” mechanism based on packaging entry points (jarvis.nodepacks).
Goal
You will:
- Wrap an existing HTTP API as a new atomic
NodeType. - Install it as a JARVIS node pack so:
Node Registryseeds theNodeTypemetadata, andAtomic Executorloads the handler.
- Execute it via the ARP
Atomic ExecutorAPI.
When to use this
- You already have a stable upstream API and want to make it callable as an ARP capability.
- You want to avoid running untrusted code (the wrapper code is your code, deployed in your stack).
Prerequisites
- Python
>=3.11 - A reachable upstream HTTP API (for this tutorial we assume it has a
GET /health) - JARVIS components available locally (recommended):
- Install
arp-jarvis(pins compatible component versions) - Run
Node Registry+Atomic Executorviaarp-jarvis node-registry ...andarp-jarvis atomic-executor ...
- Install
For a “no Keycloak” local run, you can disable inbound auth with ARP_AUTH_MODE=disabled (dev only).
Step 0: Start a tiny demo upstream API (optional)
If you don’t already have an upstream API to wrap, start a tiny local server that exposes GET /health:
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802
if self.path != "/health":
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"ok")
HTTPServer(("127.0.0.1", 9999), Handler).serve_forever()
PY
How node packs work in JARVIS v0.x
JARVIS discovers node packs via Python packaging entry points:
- Entry point group:
jarvis.nodepacks - Each entry point resolves to a
NodePackobject (metadata + handlers)
Then:
Node RegistryloadsNodeTypemetadata from all installed packs and seeds them into its DB on startup.Atomic Executorloads handlers from all installed packs and executes them bynode_type_id.
Step 1: Create a node pack package
Create a new folder (name it anything; acme-jarvis-nodepack is an example):
mkdir -p acme-jarvis-nodepack/src/acme_jarvis_nodepack
cd acme-jarvis-nodepack
Create pyproject.toml:
[build-system]
requires = ["setuptools>=70", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "acme-jarvis-nodepack"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"arp-jarvis-atomic-nodes[runtime]==0.3.3",
]
[project.entry-points."jarvis.nodepacks"]
"acme.thin" = "acme_jarvis_nodepack.pack:pack"
[tool.setuptools]
package-dir = { "" = "src" }
[tool.setuptools.packages.find]
where = ["src"]
Create src/acme_jarvis_nodepack/__init__.py:
__version__ = "0.1.0"
Step 2: Implement a wrapper node
Create src/acme_jarvis_nodepack/health_node.py:
from __future__ import annotations
import os
from jarvis_atomic_nodes.sdk import NodeContext, atomic_node
HEALTH_INPUT_SCHEMA = {
"type": "object",
"additionalProperties": False,
"properties": {
"base_url": {"type": ["string", "null"]},
},
"required": ["base_url"],
}
HEALTH_OUTPUT_SCHEMA = {
"type": "object",
"additionalProperties": False,
"properties": {
"status_code": {"type": "integer"},
"text": {"type": "string"},
},
"required": ["status_code", "text"],
}
@atomic_node(
name="acme.api.health",
side_effect="read",
trust_tier="first_party",
egress_policy="external_http",
input_schema=HEALTH_INPUT_SCHEMA,
output_schema=HEALTH_OUTPUT_SCHEMA,
)
async def acme_api_health(inputs: dict, ctx: NodeContext) -> dict:
\"\"\"Call `GET /health` on an existing service and return the raw response text.\"\"\"
base_url = (inputs.get("base_url") or os.environ.get("ACME_API_BASE_URL") or "").strip()
if not base_url:
raise ValueError("Missing base URL (set ACME_API_BASE_URL or pass base_url)")
import httpx
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f\"{base_url.rstrip('/')}/health\")
return {"status_code": resp.status_code, "text": resp.text}
Notes:
- This node uses
httpxonly at runtime (imported inside the handler), so metadata discovery can stay lightweight. - In v0.x,
Atomic Executorruns this code in-process; treat it as trusted.
Step 3: Export a NodePack
Create src/acme_jarvis_nodepack/pack.py:
from __future__ import annotations
from jarvis_atomic_nodes.sdk import NodePack
from . import __version__
from .health_node import acme_api_health
pack = NodePack(
pack_id="acme.thin",
version=__version__,
nodes=[acme_api_health],
)
Step 4: Install the pack (local dev)
In the pack folder:
python3 -m pip install -e .
Step 5: Run Node Registry and Atomic Executor
Run Node Registry (seeds NodeTypes from installed packs on startup):
export ARP_AUTH_MODE=disabled
export JARVIS_NODE_REGISTRY_DB_URL=sqlite:///./jarvis_node_registry.sqlite
export JARVIS_NODE_REGISTRY_SEED=true
arp-jarvis node-registry -- --host 127.0.0.1 --port 8084
Run Atomic Executor (loads handlers from installed packs on startup):
export ARP_AUTH_MODE=disabled
export ACME_API_BASE_URL=http://127.0.0.1:9999
arp-jarvis atomic-executor -- --host 127.0.0.1 --port 8082
Step 6: Verify discovery + execution
Verify the NodeType exists in Node Registry:
curl -sS http://127.0.0.1:8084/v1/node-types?q=acme.api.health | python3 -m json.tool
Execute the node directly via Atomic Executor:
curl -sS -X POST http://127.0.0.1:8082/v1/atomic-node-runs:execute \
-H 'Content-Type: application/json' \
-d '{
"run_id": "run_demo",
"node_run_id": "node_run_demo",
"node_type_ref": {"node_type_id": "acme.api.health", "version": "0.1.0"},
"inputs": {}
}' | python3 -m json.tool
You should get an AtomicExecuteResult with:
state: "completed"(on success)outputsmatching your output schema (status_code,text)
Troubleshooting
unknown_node_typefrom Atomic Executor: the pack was not installed in the executor environment, or the entry point is missing.- Node Registry doesn’t show the node type:
JARVIS_NODE_REGISTRY_SEED=false, or the pack is not installed, or the entry point is wrong. 401/403from either service: setARP_AUTH_MODE=disabledfor local dev, or configure JWT properly.- HTTP failures: verify
ACME_API_BASE_URLand that the upstream service is reachable.
Production note (how this gets deployed)
In production, install your node pack into the same environments/images as:
Node Registry(so it can seed theNodeTypemetadata), andAtomic Executor(so it can execute the handler).
If you deploy via JARVIS_Release (recommended), the stack consumes version-pinned GHCR images by default.
To include your custom pack, build derived images from the pinned upstream images (install your pack with pip),
then point Docker Compose to your derived images (or use a small compose override file).
Next steps
- How-to: Wrap an HTTP API as an atomic node
- Concept: Candidate sets
- Reference: ARP Standard: Atomic Executor