This guide covers running the rustscript-agent-gateway binary (and the
single-run rustscript-agent binary) outside a development checkout. It
states current behavior only; anything marked not implemented must not be
treated as available.
rustscript-agent depends on the RustScript VM through a path
dependency (pd-vm = { path = "../rustscript", ... } in Cargo.toml).
Consequences, stated plainly:
- A build requires a checkout of
rustscript-lang/rustscriptnext to this repository (the agent repo at<root>/rustscript-agent, the core repo at<root>/rustscript), at a revision whosepd-vm0.1.0 matches this repository'sCargo.lock. - The lockfile in this revision was generated against core revision
06b37fd155be2b81ba4b41dbb6514e7b283f4f10(branchplan/callable-stream-integration).cargo build/test --lockedfails if the checked-out core revision no longer matches. - There is no crates.io release of this crate, and none can be made until
the core (
pd-vm,pd-host-function, and their dependency edges) is merged and published and the path dependency is replaced by a version dependency. CI pins the same core revision for exactly this reason. - Deployments therefore run from a source build of a pinned agent commit plus a pinned core commit; record both revisions together in the deployment manifest. Do not claim a "release" for a build of this branch.
# sibling checkouts required (see section 1)
git clone https://github.com/rustscript-lang/rustscript-agent.git
git clone https://github.com/rustscript-lang/rustscript.git
cd rustscript-agent
git checkout <pinned agent revision>
cd ../rustscript && git checkout 06b37fd155be2b81ba4b41dbb6514e7b283f4f10 && cd ../rustscript-agent
cargo build --release --lockedArtifacts: target/release/rustscript-agent (single-run runner) and
target/release/rustscript-agent-gateway (HTTP gateway). There are no other
binaries.
The gateway is configured entirely by environment variables (see
docs/configuration.md). Minimal production-style start:
export RUSTSCRIPT_AGENT_GATEWAY_ADDR=127.0.0.1:8090
export RUSTSCRIPT_AGENT_BEARER_TOKEN='<secret>' # see section 9
export RUSTSCRIPT_AGENT_ALLOW_HOSTS=api.example.com
export RUSTSCRIPT_AGENT_ALLOW_PORTS=443
export RUSTSCRIPT_AGENT_SCRIPT=/etc/rustscript-agent/agent.rss
export RUSTSCRIPT_AGENT_STATE_DB=/var/lib/rustscript-agent/state.db
# Optional (A7): bounded rate limiting and the client-disconnect policy.
export RUSTSCRIPT_AGENT_RATE_LIMIT_ENABLED=1
export RUSTSCRIPT_AGENT_CLIENT_DISCONNECT_POLICY=keep-running
# Optional (A8): Telegram adapter; without a token the adapter stays off.
export RUSTSCRIPT_AGENT_TELEGRAM_BOT_TOKEN='<secret>' # see section 9
exec ./target/release/rustscript-agent-gatewayStartup failure modes (all exit non-zero before serving):
- unparsable
RUSTSCRIPT_AGENT_GATEWAY_ADDR; - missing or blank
RUSTSCRIPT_AGENT_BEARER_TOKEN(unlessRUSTSCRIPT_AGENT_ALLOW_ANONYMOUS=1); RUSTSCRIPT_AGENT_ALLOW_PORTSwith an empty entry or no valid ports;RUSTSCRIPT_AGENT_RATE_LIMIT_*values outside their validated bounds (bursts above 1 000 000, a window above 86 400 000 ms, or a non-0/1RUSTSCRIPT_AGENT_RATE_LIMIT_ENABLED);RUSTSCRIPT_AGENT_CLIENT_DISCONNECT_POLICYwith an unknown spelling;- a blank
RUSTSCRIPT_AGENT_TELEGRAM_BOT_TOKENor an invalidRUSTSCRIPT_AGENT_TELEGRAM_API_BASE(non-https remote origin, embedded credentials, query, fragment, or path); - unreadable
RUSTSCRIPT_AGENT_SCRIPTor a source over 1 MiB / failing to compile; - an unwritable or invalid
RUSTSCRIPT_AGENT_STATE_DBpath.
The legacy PD_EDGE_AGENT_* aliases still work but print a deprecation
warning; they are scheduled for removal before v1.
RUSTSCRIPT_AGENT_STATE_DBnames the state file. The parent directory must exist and be writable by the gateway user; it is the SQLitedatabase_rootfor the RSS storage program.- The file is opened with
read_write_createmode through the core SQLite host with a 5 000 ms busy timeout; the core does not enable WAL, so the default rollback-journal behavior applies. - One gateway process per state file. The storage worker owns a single
connection and SQLite serializes writers; a second gateway on the same
file contends for the writer slot and fails with
SQLITE_BUSYonce the 5 000 ms busy timeout is exhausted. The gateway is not designed for multi-process access to one state file; use one file per instance. - Without
RUSTSCRIPT_AGENT_STATE_DBthe gateway runs fully in memory: sessions, runs, jobs, and events are lost on restart.
- The HTTP policy is deny-by-default and enforced by the
pd-vmcore at request time: every destination host and port must be allowlisted (RUSTSCRIPT_AGENT_ALLOW_HOSTS,RUSTSCRIPT_AGENT_ALLOW_PORTS), schemes default tohttps,wss, and private/loopback IP destinations are rejected unlessRUSTSCRIPT_AGENT_ALLOW_PRIVATE_IPS=1. With the default configuration no script can make any HTTP request at all. - The gateway's own listener binds per
RUSTSCRIPT_AGENT_GATEWAY_ADDR(default127.0.0.1:8090). TLS is not implemented; terminate TLS in a reverse proxy in front of the gateway. - Requests are authorized with an
Authorization: Bearer ***header (constant-time token comparison) when a token is configured; every route — including/health/detailedand/metrics— sits behind the middleware. Admission is additionally bounded bymax_concurrent_runs(native config, 8 by default) and the body limit (4 MiB by default). - Rate limiting (A7) is implemented and disabled by default
(
RUSTSCRIPT_AGENT_RATE_LIMIT_ENABLED=1turns it on): one bounded token-bucket per peer IP and one per verified bearer account, both refilling over one window; an exhausted bucket answers429with aRetry-Afterheader. Failed authentication never charges an account bucket. The limiter is middleware on the API router only — the Telegram adapter's Bot API outbound traffic is never rate-limited by it. Budget your metrics scraper accordingly:/metricscounts against the peer-IP budget like every other route, and there is no private exemption. - Run execution is bounded:
run_timeout(default 900 s), fuel (10 000 000 default), per-run event caps (max_events_per_run240,max_event_bytes32 KiB), and a 5 s cancellation grace. - Client-disconnect policy (A7):
keep-running(default) survives any subscriber disconnect and events stay replayable through theafter_seqcursor;cancel-on-disconnectcancels the run with the typedclient_disconnectreason only when the LAST subscriber disconnects while the run is still active (multi-subscriber and reconnect races can never cancel while one subscriber remains).
| Endpoint | Meaning |
|---|---|
GET /health/detailed |
Liveness + minimal readiness: {"status":"ok","active_agents":N,"terminal_pending":N,"agent":"local-rss-agent"}. terminal_pending reports runs whose terminal commit is awaiting the bounded durable retry (observable instead of a silent leak). |
GET /metrics |
Prometheus text exposition of the bounded metrics registry (admissions, active runs, terminals, storage ops, SSE subscribers, run durations). Reads atomics only — the scrape never blocks on the store. Requires the bearer token when one is configured and counts against the per-IP rate-limit budget. |
GET /v1/models |
Returns the configured model id; doubles as a plain reachability probe. |
Both require the bearer token when one is configured. A healthy process is
one that answers; terminal_pending > 0 is not a crash condition but
indicates the storage side was recently unavailable. Metrics and health
share one atomic snapshot, so the two endpoints can never disagree.
- The gateway handles Ctrl-C (
SIGINT) throughtokio::signal::ctrl_cwith a bounded, ordered drain:- Stop admission: new runs answer the typed
gateway_haltingrejection (HTTP 503), so no new work can start. - Stop Telegram (when enabled): the poller stops, the final getUpdates offset is persisted, and the join is bounded at 60 s. The reconnect task can never spawn a second adapter mid-shutdown.
- Cancel active runs with the typed
resource-closedreason; workers exit within their configured bounds and commit their typed terminal transitions. - Close the storage worker deterministically: queued commands fail
fast with a typed
storage_unavailableerror instead of hanging. The process then exits.
- Stop admission: new runs answer the typed
SIGTERMis not caught by the current binary. Under systemd, setKillSignal=SIGINTso the graceful path runs; a plainSIGTERMkills the process immediately. SQLite recovers the file on next start (journal rollback), but in-flight in-memory run state is lost.- An interrupted process is repaired on restart: interrupted runs are
converted to a documented terminal state during load, exactly once, and
pending terminal commits are retried within
terminal_commit_retry_window(default 300 s). Every pending compaction is failed by restart recovery (any pending row after a restart is an interrupted leftover), so a crash between the run terminal commit andcompaction.failcan never leave a session stuck.
The Telegram adapter shares the same AgentService and SQLite store as the
API server; it is enabled by setting RUSTSCRIPT_AGENT_TELEGRAM_BOT_TOKEN.
- Transport: Bot API calls go out over https through a rustls TLS
connector. The api_base must be a bare origin — no credentials, query,
fragment, or path (the token is embedded in the request URL by the Bot
API protocol, so anything else could smuggle it). An
httpbase is rejected unless the host is localhost ANDRUSTSCRIPT_AGENT_TELEGRAM_ALLOW_INSECURE_LOCALHOST=1(test fixtures only). The adapter's outbound traffic is not affected by the API rate limiter. - Allowlists are deny-by-default:
allowed_accounts(bot account usernames),allowed_chats, andallowed_usersall start empty and an empty list denies everything. Configure all three before enabling the token in production; a denied sender gets a plain "not allowed" reply. - Retries are bounded: 429 answers sleep
retry_after(capped atmax_429_backoff, 30 s by default) for at most 3 rounds; 5xx answers use capped exponential backoff for at most 3 rounds; an unauthorized failure bound (3 by default) parks the adapter in a degraded state instead of hammering the API with an invalid token. - First boot is fail-closed: by default updates queued while the bot
was offline are drained and dropped before polling starts
(
RUSTSCRIPT_AGENT_TELEGRAM_DROP_PENDING_UPDATES=0opts into processing them). Delivery of run events is at-least-once through durable delivery cursors: a message may be delivered twice after a crash, never silently lost. - Degraded startup never kills the API: if
getMe/network fails at startup, the adapter retries in the background with bounded backoff (3 attempts) and is then disabled for the process; the API server keeps serving. The reconnect task is also cancelled by the graceful shutdown path (section 7). - Shutdown drains: on SIGINT the poller stops, the final getUpdates offset is persisted, and the join is bounded (section 7).
- Stop the gateway (graceful shutdown, section 7), then copy the state file.
Because WAL is not enabled, a single-file copy taken while the gateway is
stopped is a consistent snapshot; remove any stale
state.db-journalleftovers only after a clean shutdown. - Restore: place the copy at the configured path and start the gateway.
- There is no online backup tooling and no migration runner in this
revision; schema changes are applied by the RSS storage program at open
time (see
rss/storage/schema.rss).
- Secrets:
RUSTSCRIPT_AGENT_BEARER_TOKENandRUSTSCRIPT_AGENT_TELEGRAM_BOT_TOKEN. Deliver them via an environment file with mode0600owned by the service user (section 11), or injected secrets, never via command-line arguments or image build args. Neither secret is ever logged: the bearer token is compared in constant time and the Telegram token is redacted in every Debug/log surface. - Logging: the binaries write startup/halt messages to stderr via
eprintln!.tracingevents exist in library code but the binaries install no tracing subscriber, soRUST_LOGhas no effect today and no structured log output is produced. Observability (A9) is implemented: a bounded metrics registry (fixed label sets — no run/session/token/model high-cardinality labels) is scraped atGET /metricsin Prometheus text format, and/health/detailedreads the same atomic snapshot. Capture stderr with your supervisor and rotate it.
# /etc/systemd/system/rustscript-agent-gateway.service
[Unit]
Description=RustScript agent gateway
After=network-online.target
[Service]
User=rustscript-agent
Group=rustscript-agent
ExecStart=/opt/rustscript-agent/bin/rustscript-agent-gateway
EnvironmentFile=/etc/rustscript-agent/gateway.env
# The binary handles SIGINT gracefully; SIGTERM would kill it immediately.
KillSignal=SIGINT
TimeoutStopSec=30
Restart=on-failure
RestartSec=2
# State and source live under /var/lib; the service user must own them.
StateDirectory=rustscript-agent
ReadWritePaths=/var/lib/rustscript-agent
[Install]
WantedBy=multi-user.target/etc/rustscript-agent/gateway.env (mode 0600):
RUSTSCRIPT_AGENT_GATEWAY_ADDR=127.0.0.1:8090
RUSTSCRIPT_AGENT_BEARER_TOKEN=[REDACTED]
RUSTSCRIPT_AGENT_ALLOW_HOSTS=api.example.com
RUSTSCRIPT_AGENT_ALLOW_PORTS=443
RUSTSCRIPT_AGENT_SCRIPT=/var/lib/rustscript-agent/agent.rss
RUSTSCRIPT_AGENT_STATE_DB=/var/lib/rustscript-agent/state.db# Build stage: requires the pinned core checkout next to the agent repo.
FROM rust:1-slim AS build
WORKDIR /src
COPY rustscript-agent/ ./rustscript-agent/
COPY rustscript/ ./rustscript/
RUN cd rustscript-agent && cargo build --release --locked --bin rustscript-agent-gateway
FROM debian:bookworm-slim
RUN useradd --system --home /var/lib/rustscript-agent rustscript-agent
COPY --from=build /src/rustscript-agent/target/release/rustscript-agent-gateway /usr/local/bin/
USER rustscript-agent
EXPOSE 8090
# Secrets via --env-file or an orchestrator secret, never baked into the image.
ENTRYPOINT ["/usr/local/bin/rustscript-agent-gateway"]Mount the state directory (/var/lib/rustscript-agent) as a volume; keep
the state file exclusive to one replica (section 4). TLS and health-check
paths are the same as sections 5–6.
rustscript-agent --script agent.rss --allow-host api.example.com compiles
and runs one script to completion and prints the Complete value. It is
synchronous and holds no state; it is suitable for cron-style invocation,
not for serving. Note that the runner exposes no port allowlist, so script
HTTP requests over non-default policy ports fail inside the VM (section 5).