Skip to content

38 · Licences, running offline, and what needs the network

The stalwart-agents package carries Stalwart's runtime, compiled: the reader that turns each turn into a plan, the tool check, the reply checks, retrieval, the code checks, the licence and the meters. It runs in your process, on your machine. A licence is what lets it run there: this machine, activated once against your account. This chapter is what runs on the machine and what needs our service, how a licence is activated, kept and released, and what happens when it lapses.

Language support

The selector reads turns written in English and in Brazilian Portuguese (pt-BR). The language is chosen, never guessed from the turn: the console's Settings page (Language the selector reads), language on a plan request, or the settings.language of your vocabulary file; with none of them, English. A Portuguese turn may be typed with its accents or without them. European Portuguese is not a grammar the selector has, and a turn in any other language is unread.

What runs where

Where the runtime is installed, a turn and a tool call need no network: the runtime answers both on the machine that runs your agent.

What happens once, before deployment or in the background may use our service. Each row is tied to the code that does the work, the runtime's operation or the service's route, and generated from it:

Work Where it runs Network
reading a turn into a plan the runtime, in your process none
checking a tool call: the catalogue, the arguments, the history, the order rules, the rulebook's gate, the marks, the confirmation tokens the runtime, in your process none
holding a reply to what was done, and screening turns and replies for personal data the runtime, in your process none
indexing your documents, retrieving passages and checking an answer against them the runtime, in your process none; compose calls your own model, with your key
the code checks of stalwart verify the runtime, in your process none
linting a catalogue the runtime, in your process none
the guarded tool step, undoing a failed run, the rewind, pauses for a person and their resume the package, over the runtime, in your process none, except the calls your own tools make
the gateway's checks the package, over the runtime, in your process none, except the calls it passes on to your tool servers
a calculation: your model writes the function and the sandbox image runs it, on your machine the package, over the runtime, in your process your model's call; the function itself runs with no network
compiling a Colang rulebook or a rule sheet to the record the tool check reads; nothing is kept our service once, when an agent or stalwart verify reads its rules; none for a folder stalwart rules compile wrote
the hunt and the proof of an agent's graph our service before deployment: at stalwart verify, and once per graph before an agent's first turn
activating the licence and refreshing the lease our service once at activation, then in the background; the runtime runs on through the grace period without it
reporting usage: counts only our service in the background, with the lease refresh
the bundles the runtime reads: the vocabulary, the reply rules and the phrasings our service in the background; a new bundle is taken at a lease refresh, never mid-conversation
your users' thumbs, for the miner our service only where you post them

Activating

$ export QLS_API_KEY=qls_live_...
$ stalwart licence activate        # once on each machine
$ stalwart licence status          # the licence, the lease, the grace and what is left of each cap
$ stalwart licence release         # frees this machine's place, for another to take

Activation makes this machine's installation -- a random id and a key pair, never a hardware fingerprint -- and exchanges your key for your organisation's licence and a first lease: the runtime's permission to run here until a date. Your plan sets how many machines may hold a lease at once, how long a lease runs, the grace period after it, and whether a machine can be activated with no connection at all:

The local runtime's licence Free Paid Enterprise
installations: machines holding a lease at once 1 5 50
lease_days: days a lease runs before it is renewed 7 7 7
offline_grace_days: days the runtime keeps working past its lease without reaching us 7 30 30
offline_activation: activating a machine that never reaches the network, by request file no no yes

Nothing in the package works without a key and an activated licence, except activating it, stalwart doctor and stalwart check. Without them a command stops with LicenceRequired, naming stalwart licence activate.

Over HTTP, activating is POST /v1/licences/activate. GET /v1/licences reads the licence and how many installations hold its seats, and GET /v1/licences/keys, which needs no key, lists the Ed25519 public keys licences and leases are signed with: a release of the runtime compiles them in and trusts no other.

Run it on my computer, from the console

A bot made in the console runs on a computer with no terminal work beyond one line. Run it on my computer, on the bot's page, asks for a ticket that lasts seven days and may be spent as often as the line is run, until a newer one is made for the bot, and shows the line for Windows or for a Mac. The line fetches the installer, GET /v1/install/{ticket}/{system}, which puts stalwart-agents in an environment of its own with a pinned uv. The installer then spends the ticket, POST /v1/install/{ticket}, which answers with the bot's definition, a key made for it then and named after it, and the person's email address, which Unpaywall's requests carry. Running the bot again on another computer revokes the key the earlier run made: a bot runs on the computer that ran it last.

The installer writes the bot as agent.yaml, with each tool that a server of the package serves run by that server on the computer (chapter 48), keeping its files in Documents/Stalwart/ under the bot's name. It activates the computer as stalwart licence activate does, starts stalwart serve --follows at log-on (a Scheduled Task on Windows, a launchd agent on a Mac), and opens the chat at http://127.0.0.1:8700/.

The removal script takes the bot off the computer again: GET /v1/install/remove/{system}, windows for a PowerShell script and mac for a shell script. It stops the bot and its start at log-on, gives the computer's licence seat back, and deletes what the installer put there, keeping the bot's own files in Documents/Stalwart/. It needs no key and no ticket: it names nobody.

The bot follows the console

POST /v1/install/{ticket} also names the bot and its current version, which the installer keeps in saved.json beside agent.yaml. With --follows, the bot reads the bot bundle every fifteen minutes: the bot as the console saves it now, sealed under the lease like every bundle, and given only to the key its installer made, with the newest stalwart-agents released.

  • A newer version that takes nothing away -- new tools, new tasks, changed settings -- is written as the bot's agent.yaml, as the installer would write it, and the bot stops with exit code 75; the run script starts it again on the new version. Its runs, kept in its storage, carry on.
  • A newer version that takes something away -- an agent, a tool, a task or a trigger it no longer has, or its runs kept elsewhere -- waits in held.json. The chat page says what it takes away, and Use the new version applies it (POST /updates/apply, with the Stalwart-Apply: yes header a page of another site cannot send).
  • A newer stalwart-agents is told of on the chat page; pressing Run it on my computer again installs it.

GET /updates answers what the chat page shows: {"held": {"version", "takes_away"} or null, "software": {"installed", "newest"} or null}.

The lease, its refresh and the grace period

The first answer the runtime gives in a process refreshes the lease with our service, so a cancelled plan or a revoked licence stops the runtime at once; the same refresh fetches the vocabulary and the other bundles and reports the counts. Where the service cannot be reached, the runtime opens the last lease as it stands: within its term, and then within the grace period, it answers as usual, and stalwart doctor and stalwart licence status show the state, active or grace, and the dates the lease and the grace end.

The refresh is POST /v1/licences/refresh. Each bundle comes from GET /v1/bundles/{kind} -- vocabulary, prompt_pack, reply_rules, outcome_phrasings, own_identifiers, language or bot -- signed, encrypted to your licence's bundle key, and named by the digest of its content, which every decision made with it records. own_identifiers is the list your console's Settings hold as your organisation's own identifiers: the SDK and stalwart scan every request bound for our service for them, beside personal data, and send nothing that holds one. language holds the words the reader reads every turn with, in each language it reads: the runtime plans no turn until it holds them, and takes only the words of its own version. One more kind is read on its own clock rather than at the refresh: bot, the bot as the console saves it, every fifteen minutes, for a bot that follows it (below).

Running offline

The shop agent of chapter 30. It starts with the network, which proves its graph and compiles its rulebook:

desk = Agent.from_file("agent.yaml")
desk.chat("show me order 4412", user="u-0")     # start-up, with the network: the proof and the registration

Then the network is cut, every socket refused, and the agent goes on:

# The network is cut here: from now on nothing leaves the machine.
reply = desk.chat("show me order 4411", user="u-17")
print(reply.text)
reply = desk.chat("charge order 4411 40.00", user="u-17")
print(reply.waiting[0].tool, "waits:", reply.waiting[0].wording)
reply = desk.confirm("u-17", "approve")
print(reply.text)
card: visa-0019
customer: c-208
order_id: 4411
status: unpaid
total: 40.00
charge_card waits: Run charge_card?
amount: 40
charge_id: ch-4411
order_id: 4411

The read was answered, the charge checked and asked about, and the approved charge run, with nothing leaving the machine but the call to the shop's own tool.

When the licence lapses

A lease and its grace both past, or the licence expired, is a lapsed licence. A safety product must not fail open, and a production agent must not stop without warning, so a lapse degrades by your organisation's on_licence_lapse setting:

  • reads_only, the default: turns are read as usual, reads are checked as usual, and every write is refused as licence_lapsed.
  • refuse: every operation is refused as licence_lapsed, with the date the lapse began.

The same agent, after its licence lapsed:

# The licence has lapsed, and the organisation's setting is `reads_only`.
reply = desk.chat("show me order 4411", user="u-18")
print(reply.text)
reply = desk.chat("charge order 4411 40.00", user="u-18")
print(reply.text)
card: visa-0019
customer: c-208
order_id: 4411
status: unpaid
total: 40.00
The licence for this agent has lapsed, so charge_card was not run.

Nothing is ever allowed under a lapse that a licensed runtime would have refused.

Machines with no connection

Where your plan allows offline activation, a machine that never reaches Stalwart is activated by file, through one that does:

$ stalwart licence request                          # on the machine to activate: writes activation-request.json
$ stalwart licence answer activation-request.json   # on a machine that reaches Stalwart: writes activation-answer.json
$ stalwart licence install activation-answer.json   # back on the first machine

The answer holds the licence and a lease for the licence's whole term. An enterprise can instead run qls-licence-server in its own network, which issues leases to its machines as the licence service does.

stalwart licence answer sends the request to POST /v1/licences/offline, which answers only for a licence that allows offline activation.

Releasing a seat

stalwart licence release frees this machine's place on the licence, so another machine can take it: what a CI job does last (chapter 46).

Over HTTP, it is POST /v1/licences/release.

What is counted, and what is reported

The runtime counts each meter your plan names -- plan_calls, tool_checks, answer_calls and the reply checks' counts among them -- in its own state, on its own clock. Past a cap, it answers quota_exhausted, naming the meter, as the service does. Counts, and only counts, are reported: never a turn, an argument, a result, a reply or a document. Your plan's caps are in chapter 16.

They go to POST /v1/usage-reports as a signed record, counted as the difference from the installation's last report, so a report sent twice is counted once, and a counter file rolled back is detected.

stalwart doctor and stalwart licence

  • stalwart doctor prints the base URL, whether a key is set (never the key), whether the local runtime answers plans and tool checks, with the licence's state, the round trip to the service, your tier, its entitlements and this month's use of each meter, and whether streaming reaches you through your network. It works with no licence, to say what is missing.
  • stalwart licence takes activate, request, answer, install, release and status.

What stalwart doctor prints of your plan is GET /v1/account, client.account() in Python: the key's tier, its entitlements and limits, and this month's use of each meter. Any key may read it.

The limits

  • Compiled code and encrypted bundles deter reading; they do not prevent it. A determined party with a licensed installation can extract what it runs. The licence terms and your contract are what stop that.
  • Some work still needs our service: the hunt and the proof of an agent's graph, compiling its rulebook, activating and refreshing a licence, and the miner. An agent's first start proves its graph and compiles its rulebook there; a machine that never reaches the service runs on its lease and grace alone.
  • Where the runtime is not installed -- a platform the wheel does not ship for -- nothing answers plans or tool checks, and our hosted API answers neither: run your agents on a platform the wheel ships for.