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 theStalwart-Apply: yesheader a page of another site cannot send). - A newer
stalwart-agentsis 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 aslicence_lapsed.refuse: every operation is refused aslicence_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 doctorprints 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 licencetakesactivate,request,answer,install,releaseandstatus.
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.