Verentis

Example (shell)

Example — a shell engine

The simplest possible engine — a few lines of bash with no SDK — showing the raw context/result protocol any language can implement.

The Python SDK removes boilerplate, but an engine is really just a container that reads /verentis/context.json and writes /verentis/result.json. Any language can do that. Here's a complete engine in a few lines of bash + jq — no SDK.

It handles .sh files with two tools: execute (run the script) and lint (bash -n syntax check).

The manifest

# sh.engine.yaml
api-version: verentis.io/v1
kind: ExecutionEngine
metadata:
  name: shell
  display-name: Shell
  description: Run and lint shell scripts.
  icon: lucide:square-terminal
  version: 0.1.0
  author: Verentis
spec:
  runtimes:
    docker:
      image: ghcr.io/you/engine-shell:0.1.0
      pull-policy: IfNotPresent
  file-types:
    - pattern: text/x-shellscript
      extensions: [.sh]
      priority: 100
  execution-modes:
    - request-response
  tools:
    - name: execute
      description: Execute the shell script and capture stdout.
      input-schema: { type: object, properties: {}, required: [] }
      output-schema:
        type: object
        properties: { status: { type: string }, data: { type: object } }
        required: [status]
    - name: lint
      description: Syntax-check the script with `bash -n`.
      input-schema: { type: object, properties: {}, required: [] }
      output-schema:
        type: object
        properties: { status: { type: string }, data: { type: object } }
        required: [status]
  capabilities: [files]
  permissions:
    - node.file.read
  resources: { cpu: "0.25", memory: "128Mi", timeout: 120, max-concurrent: 5 }
  sandbox: { network: none, read-only-root: true, no-new-privileges: true }

The entrypoint

The platform mounts the target script at /verentis/script.sh and the context at /verentis/context.json. Read the tool, do the work, write the result:

#!/usr/bin/env bash
# entrypoint.sh — the whole engine.
set -euo pipefail

CTX=/verentis/context.json
OUT=/verentis/result.json
SCRIPT=/verentis/script.sh

tool=$(jq -r '.execution.tool // "execute"' "$CTX")

# Emit a result document in the shape the platform expects.
emit() { jq -n --arg status "$1" --argjson data "$2" \
  '{status: $status, artifacts: [], data: $data}' > "$OUT"; }

case "$tool" in
  execute)
    if out=$(bash "$SCRIPT" 2>&1); then
      emit success "$(jq -n --arg o "$out" '{stdout: $o}')"
    else
      code=$?
      jq -n --arg o "$out" --argjson c "$code" \
        '{status:"failed", artifacts:[], error:$o, data:{exitCode:$c}}' > "$OUT"
      exit "$code"
    fi
    ;;
  lint)
    if err=$(bash -n "$SCRIPT" 2>&1); then
      emit success '{"diagnostics": []}'
    else
      emit failed "$(jq -n --arg e "$err" '{diagnostics: [$e]}')"
    fi
    ;;
  *)
    jq -n --arg t "$tool" '{status:"failed", artifacts:[], error:("unknown tool: "+$t)}' > "$OUT"
    exit 1
    ;;
esac

The image

FROM alpine:3.20
RUN apk add --no-cache bash jq
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

Try it locally

Same mock-context flow as any engine (see Testing your engine):

docker build -t engine-shell:dev .

mkdir -p .local/verentis
echo 'echo "hello from a shell engine"' > .local/verentis/script.sh
cat > .local/verentis/context.json <<'JSON'
{ "version": 1,
  "execution": { "id": "local", "mode": "request-response", "tool": "execute", "trigger": "user", "timeout": 120 },
  "workspace": { "id": "ws-local" },
  "file": { "path": "/scripts/hello.sh", "branch": "main" },
  "input": { "arguments": [], "environment": {}, "parameters": {} } }
JSON

docker run --rm -v "$PWD/.local/verentis:/verentis" engine-shell:dev
cat .local/verentis/result.json
# -> {"status":"success","artifacts":[],"data":{"stdout":"hello from a shell engine"}}

Reading & writing files without an SDK

This example doesn't touch the VFS. To read/write workspace files from a no-SDK engine, call the gateway directly with curl using platform.apiUrl + platform.token.accessToken from the context — the same /{version}/files/... API the SDKs wrap. See Files & nodes.

Where to go from here