Skip to content

Repository files navigation

tinypaw

tinypaw is a queue-backed agent harness with a Node.js CLI client, an HTTPS/SSE gateway, Redis, BullMQ, and the OpenAI Responses API. It can use either the hosted OpenAI API or a local OpenAI-compatible server such as LM Studio.

Current capabilities

  • One-shot and interactive CLI sessions.
  • Asynchronous turn processing through BullMQ.
  • Session events, leases, skills, configuration, and usage limits in Redis.
  • Multi-step Responses API loop with custom function tools.
  • Incremental model output from typed Responses API streaming events.
  • SSE and persistent WebSocket provider transports.
  • Hosted OpenAI and local loopback endpoints.
  • Automatic local model discovery through GET /v1/models.
  • Authenticated HTTPS turn submission with SSE lifecycle/result streaming.
  • OpenAI-compatible /v1/models and /v1/responses endpoints for external clients, including typed Responses API streaming.
  • Static checks, unit tests, and Redis integration tests.

Architecture

CLI/API client <── HTTPS/SSE ──> gateway
                                    ├── session/skill APIs ──> Redis 8
                                    └── enqueue ──> BullMQ ──> worker
                                                                  ├──> Redis 8
                                                                  └──> Responses API
                                                                       (SSE/WebSocket)

The worker currently exposes two tools to the model:

  • load_skill loads a named skill from Redis.
  • load_config loads a JSON configuration namespace from Redis.

The CLI is a gateway-only client. It does not receive Redis credentials, use BullMQ, or connect to the model provider. The gateway owns queue submission and session/skill APIs; only the worker calls the model.

Requirements

  • Node.js 20 or newer.
  • Docker with Compose, or an existing Redis 8 server.
  • A TLS certificate and private key for the gateway; OpenSSL is optional for generating the documented development certificate.
  • One of:
    • an OpenAI API key;
    • a local server implementing /v1/models and streaming /v1/responses over SSE or WebSocket.

Installation

npm install
cp .env.example .env
docker compose up -d redis

Choose either the local or hosted provider configuration below, configure the GATEWAY_* client/server values as described under Native HTTPS + SSE gateway API, then start the worker and gateway. After both are ready, run:

npm start -- doctor

doctor exits with a non-zero status if the gateway is unavailable or the CLI Bearer token is missing. Gateway health covers its BullMQ/Redis queue connection. Provider connectivity is exercised by the worker when it processes a turn, not by doctor.

Local OpenAI-compatible API

LM Studio exposes the required OpenAI-compatible endpoints on port 1234 by default. Start its local server, then configure .env:

OPENAI_API_KEY=
OPENAI_BASE_URL=http://127.0.0.1:1234
OPENAI_MODEL=
OPENAI_TRANSPORT=sse

Behavior in local mode:

  • /v1 is appended automatically when OPENAI_BASE_URL contains only an origin.
  • An API key is optional for 127.0.0.1, localhost, and ::1.
  • With an empty OPENAI_MODEL, the first identifier returned by /v1/models is selected.
  • Set OPENAI_MODEL explicitly when the server exposes multiple generation models:
OPENAI_MODEL=google/gemma-4-12b

The local server must support typed response.created, response.output_text.delta, and response.completed events and custom function tools. With OPENAI_TRANSPORT=sse, it must accept stream: true Responses requests; with websocket, it must accept response.create messages. These event shapes follow the OpenAI streaming Responses guide. LM Studio documents its endpoints in the OpenAI compatibility guide.

Provider settings are read from environment variables and .env. local.json is not read by the application.

Hosted OpenAI API

Leave OPENAI_BASE_URL empty and set the API key:

OPENAI_API_KEY=your-api-key
OPENAI_BASE_URL=
OPENAI_MODEL=
OPENAI_TRANSPORT=websocket

When OPENAI_MODEL is empty in hosted mode, the harness uses gpt-5.6-luna. An explicit model overrides this default:

OPENAI_MODEL=gpt-5.6-terra

For a non-loopback custom endpoint, an API key is still required.

Responses WebSocket mode

Set OPENAI_TRANSPORT=websocket to use the Responses API WebSocket mode. The worker derives wss://api.openai.com/v1/responses for hosted OpenAI, or converts an HTTP(S) OPENAI_BASE_URL to ws(s) and appends /responses. Use OPENAI_WEBSOCKET_URL only when a compatible provider exposes a different full WebSocket endpoint:

OPENAI_TRANSPORT=websocket
OPENAI_WEBSOCKET_URL=wss://provider.example/custom/responses

One WebSocket is opened per queued agent turn and reused across its sequential model/tool steps. Each step sends response.create; continuation sends only the new tool outputs plus previous_response_id. stream and background are omitted because streaming is implicit in WebSocket mode. The socket is closed when the turn completes or fails.

This setting changes only the worker-to-provider transport. The CLI continues to use HTTPS plus SSE with the tinypaw gateway.

Running the harness

The worker, gateway, and CLI run as separate processes.

Terminal 1:

npm run worker

Terminal 2:

npm run gateway

Terminal 3, one prompt:

npm start -- run "Explain the current project"

Interactive chat:

npm start -- chat

Continue using a known session:

npm start -- chat --session <session-id>
npm start -- run "Continue" --session <session-id>

Reusing a session preserves its event stream and mutual-exclusion lease, but does not replay earlier messages into later model calls. It is not currently full conversational memory.

OpenAI-compatible API

External clients can use the gateway as an OpenAI-compatible Responses API. Set their base URL to https://<gateway-host>:<port>/v1 and use the gateway API token as the OpenAI API key.

The compatibility surface exposes:

GET  /v1/models
GET  /v1/models/<model>
POST /v1/responses

GATEWAY_OPENAI_MODEL defines the virtual model ID exposed to clients and defaults to tinypaw. It is an agent alias, separate from the provider model selected by OPENAI_MODEL or Redis configuration.

The official OpenAI Node SDK can use the gateway directly:

import OpenAI from 'openai';

const client = new OpenAI({
  apiKey: process.env.GATEWAY_API_TOKEN,
  baseURL: 'https://localhost:8443/v1',
});

const stream = await client.responses.create({
  model: 'tinypaw',
  input: 'Explain the current project',
  stream: true,
});

for await (const event of stream) {
  if (event.type === 'response.output_text.delta') {
    process.stdout.write(event.delta);
  }
}

When using the documented self-signed development certificate, configure the client runtime to trust secrets/gateway-tls-cert.pem.

Supported request fields are model, input, instructions, and stream. input may be a non-empty string or a non-empty Responses API input array. Other fields return an OpenAI-shaped unsupported_parameter error instead of being silently ignored.

With stream: false or no stream field, the request waits for the queued turn and returns a completed Responses API JSON object. With stream: true, the gateway returns text/event-stream and emits:

response.created
response.in_progress
response.output_item.added
response.content_part.added
response.output_text.delta
response.output_text.done
response.content_part.done
response.output_item.done
response.completed

Each compatibility request creates a new tinypaw session. Client-supplied tools, previous_response_id, background mode, structured output controls, and the other Responses API parameters are not currently supported. The worker's built-in load_skill and load_config tool loop still runs internally.

Native HTTPS + SSE gateway API

The native gateway API accepts turns over HTTPS and streams BullMQ lifecycle events and the terminal result as text/event-stream.

Submit a turn with the API token:

curl --fail \
  --cacert secrets/gateway-tls-cert.pem \
  -H "Authorization: Bearer <gateway-api-token>" \
  -H "Content-Type: application/json" \
  -d '{"input":"Explain the current project","sessionId":"api-demo"}' \
  https://localhost:8443/v1/turns

The 202 response contains jobId, turnId, sessionId, and a relative eventsUrl. Connect to that URL with curl -N, fetch(), or browser EventSource:

curl --fail --no-buffer \
  --cacert secrets/gateway-tls-cert.pem \
  "https://localhost:8443/v1/turns/<job-id>/events?token=<scoped-token>"

The stream emits BullMQ state events and forwards normalized model events:

response.created
response.output_text.delta
response.completed

response.completed marks one model step, including steps that request a function call. The separate completed or failed event terminates the whole queued turn. The CLI prints each delta immediately and avoids printing the same final result twice.

The job-scoped token in eventsUrl allows native EventSource to authenticate without exposing the main API token. Treat the URL as sensitive and close the client after the turn-level terminal event.

Bearer-authenticated endpoints also provide remote CLI data operations:

GET /v1/sessions
GET /v1/sessions/<session-id>
GET /v1/skills
GET /v1/skills/<skill-name>
PUT /v1/skills/<skill-name>  {"content":"..."}

For host-side development, configure the GATEWAY_* values in .env and run npm run gateway. The production Compose service supplies those values and files through its container configuration.

Production Docker Compose

The production stack is defined in compose.prod.yml and contains:

  • redis: private Redis 8 with password authentication, AOF persistence, and a named volume;
  • worker: the continuously running Node.js BullMQ worker;
  • gateway: the HTTPS ingress, native SSE job stream, and OpenAI-compatible Responses API;
  • cli: an on-demand tools-profile service for CLI commands.

Create the production environment file and replace every placeholder:

cp .env.production.example .env.production

Create the gateway secret files referenced by .env.production:

mkdir -p secrets
openssl rand -hex 32 > secrets/gateway-api-token.txt

Place a trusted certificate chain and its matching private key at:

secrets/gateway-tls-cert.pem
secrets/gateway-tls-key.pem

For loopback development only, a short-lived self-signed certificate can be created with:

openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
  -keyout secrets/gateway-tls-key.pem \
  -out secrets/gateway-tls-cert.pem \
  -days 1 -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,DNS:gateway,IP:127.0.0.1" \
  -addext "extendedKeyUsage=serverAuth"

For host-side development with that certificate, set:

GATEWAY_URL=https://localhost:8443
GATEWAY_API_TOKEN_FILE=./secrets/gateway-api-token.txt
GATEWAY_CA_CERT_FILE=./secrets/gateway-tls-cert.pem
GATEWAY_TLS_CERT_FILE=./secrets/gateway-tls-cert.pem
GATEWAY_TLS_KEY_FILE=./secrets/gateway-tls-key.pem

The entire secrets/ directory is gitignored. Ensure the files are readable by Docker, then replace the remaining environment placeholders.

For hosted OpenAI, set OPENAI_API_KEY. For LM Studio running on the Docker host, use:

OPENAI_API_KEY=
OPENAI_BASE_URL=http://host.docker.internal:1234
OPENAI_MODEL=google/gemma-4-12b
OPENAI_TRANSPORT=sse

Build and start the full production stack:

docker compose \
  --env-file .env.production \
  -f compose.prod.yml \
  up -d --build

Inspect status and logs:

docker compose --env-file .env.production -f compose.prod.yml ps
docker compose --env-file .env.production -f compose.prod.yml logs -f worker
docker compose --env-file .env.production -f compose.prod.yml logs -f gateway

Run CLI commands inside the production network:

docker compose --env-file .env.production -f compose.prod.yml run --rm cli doctor
docker compose --env-file .env.production -f compose.prod.yml run --rm cli run "Hello"
docker compose --env-file .env.production -f compose.prod.yml run --rm cli chat
docker compose --env-file .env.production -f compose.prod.yml run --rm cli session list

Stop containers while preserving Redis data:

docker compose --env-file .env.production -f compose.prod.yml down

The named Redis volume survives down and container replacement. The following command also deletes persisted Redis data and must only be used when that is intentional:

docker compose --env-file .env.production -f compose.prod.yml down --volumes

Production hardening currently included:

  • Redis is not published to a host port.
  • Redis starts with appendonly yes and appendfsync everysec.
  • Worker and gateway wait for the Redis healthcheck; the tools-profile CLI waits for the gateway healthcheck.
  • Node runs as the non-root node user in a read-only container filesystem.
  • Linux capabilities are dropped and no-new-privileges is enabled.
  • Worker receives a two-minute graceful shutdown window for in-flight jobs.
  • Gateway terminates TLS 1.2 or newer and reads its API token, certificate, and private key through read-only Docker secrets.
  • Only gateway publishes a host port. It uses separate ingress and private backend networks; CLI joins only ingress, while only worker joins the provider egress network.
  • Container logs rotate at 10 MB with three retained files.

CLI reference

Command Description
npm start -- run "<prompt>" Queue one turn and print the result.
npm start -- chat Start an interactive session.
npm start -- session list List stored sessions.
npm start -- session show <id> Print session metadata and recent events.
npm start -- skill list List stored skills.
npm start -- skill get <name> Print a skill.
npm start -- skill put <name> "<content>" Create or replace a skill.
npm start -- doctor Check gateway TLS/readiness and CLI token configuration.

If skill content is omitted, skill put prompts for it interactively:

npm start -- skill put reviewer

Environment variables

Variable Default Purpose
OPENAI_API_KEY none Hosted/custom API credential; optional for loopback endpoints.
OPENAI_BASE_URL OpenAI default Custom API origin or base path. /v1 is appended only to an origin-only URL.
OPENAI_MODEL provider-dependent Hosted default is gpt-5.6-luna; local default is the first discovered model.
OPENAI_TRANSPORT sse Worker provider transport: sse or websocket.
OPENAI_WEBSOCKET_URL derived Optional full ws:// or wss:// Responses endpoint override.
OPENAI_TIMEOUT_MS 120000 hosted, 30000 local OpenAI-compatible request timeout.
REDIS_HOST localhost Redis hostname.
REDIS_PORT 6379 Redis port.
REDIS_PASSWORD none Optional Redis password.
REDIS_CONNECT_TIMEOUT_MS 3000 Redis connection timeout.
QUEUE_NAME agent BullMQ queue name; it must match between gateway and worker.
MAX_RETRIES 1 Maximum BullMQ job attempts.
WORKER_CONCURRENCY 1 Jobs processed concurrently by one worker.
JOB_TIMEOUT_MS 120000 Maximum time a CLI waits for the terminal SSE result.
MAX_AGENT_STEPS 6 Responses API iterations allowed per turn.
MAX_TOOL_CALLS 8 Function calls allowed per turn.
MAX_AGENT_BYTES 32768 Input and tool-result byte budget per turn.
SESSION_TTL_SECONDS 604800 Redis lifetime for session and sandbox data.
GATEWAY_URL https://localhost:8443 Gateway base URL used by the CLI; HTTPS is required.
GATEWAY_API_TOKEN none Bearer token used by direct gateway/CLI processes; the production containers use GATEWAY_API_TOKEN_FILE.
GATEWAY_API_TOKEN_FILE none File containing the Bearer token; read by the gateway and CLI.
GATEWAY_CA_CERT_FILE none Additional PEM CA certificate used by the CLI.
GATEWAY_CONNECT_TIMEOUT_MS 3000 CLI gateway connection timeout.
GATEWAY_HOST 0.0.0.0 Gateway listen address inside its process/container.
GATEWAY_PORT 8443 Gateway HTTPS listen port.
GATEWAY_TLS_CERT_FILE none PEM certificate chain path; required by gateway.
GATEWAY_TLS_KEY_FILE none PEM private-key path; required by gateway.
GATEWAY_MAX_BODY_BYTES 65536 Maximum JSON request body size.
GATEWAY_POLL_INTERVAL_MS 250 BullMQ state polling interval per SSE stream.
GATEWAY_HEARTBEAT_MS 15000 SSE comment heartbeat interval.
GATEWAY_OPENAI_MODEL tinypaw Virtual model ID exposed by the gateway's OpenAI-compatible API.

Production Compose additionally uses GATEWAY_BIND_ADDRESS (default 0.0.0.0), GATEWAY_HTTPS_PORT (default 8443) for host-side publishing, and GATEWAY_INTERNAL_URL (default https://gateway:8443) for its on-demand CLI container. The internal hostname must be present in the TLS certificate. It also accepts APP_IMAGE (default tinypaw:latest) and REDIS_VOLUME_NAME (default tinypaw-redis-data).

The Redis configuration stored under harness:config:default, when present, can override the agent's model, instructions, and limit values.

Redis data

The harness uses these key families:

harness:session:<session-id>
harness:session:<session-id>:events
harness:session:<session-id>:lease
harness:sandbox:<turn-id>
harness:skill:<name>
harness:config:<namespace>

BullMQ creates its own keys using the configured queue name.

Atomic session, lease, and sandbox transitions use the tinypaw Redis Function library. The application installs or replaces the library on its first atomic operation and invokes its functions with FCALL.

Validation

Run static syntax checks:

npm run check

Run the complete test suite with Redis available:

npm test

The Redis integration tests use unique key names and remove their data after completion.

Troubleshooting

OPENAI_API_KEY_MISSING

Set OPENAI_API_KEY for the hosted API, or set OPENAI_BASE_URL to a local loopback endpoint.

OPENAI_MODELS_REQUEST_FAILED

Make sure the local server is running and that this request succeeds:

curl http://127.0.0.1:1234/v1/models

OPENAI_TRANSPORT_INVALID

Set OPENAI_TRANSPORT to exactly sse or websocket.

OPENAI_WEBSOCKET_URL_INVALID

When overriding the derived endpoint, provide a full ws:// or wss:// URL in OPENAI_WEBSOCKET_URL.

LOCAL_OPENAI_MODEL_NOT_FOUND

Load or download a generation model in the local server. Alternatively, set OPENAI_MODEL to a valid model identifier returned by /v1/models.

Job timeout

Make sure the gateway and worker are running and that both use the same Redis instance and QUEUE_NAME. A client timeout or SSE disconnect does not cancel the queued job. Increase JOB_TIMEOUT_MS for slow local models.

Gateway TLS or authorization failure

Check that GATEWAY_URL uses https://, the URL hostname is present in the certificate SANs, GATEWAY_CA_CERT_FILE trusts the issuing CA/certificate, and the CLI token matches the gateway token. There is no insecure TLS bypass.

AGENT_LIMIT_EXCEEDED

The turn exceeded its step, tool-call, or byte budget. Review MAX_AGENT_STEPS, MAX_TOOL_CALLS, and MAX_AGENT_BYTES.

About

AI Agent Harness with Isolated Sandbox Runtime

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages