Concepts
This page lists what Compute Gateway does today. Where something is missing or does not work against the live services, it says “Not yet”.
Modules
A module is a WebAssembly file plus a capability list: the capabilities
it needs. You publish both to Drive under your app id, as
{name}@{version}. A version can’t be changed once published. Compute Gateway
fetches the module from Drive on every call; you name it with a module_ref:
{"owner_uid": "<the person's id>", "app_id": "acme.hello", "name": "main", "version": "1.0.0"}
owner_uid is the person the module runs for, and the only caller Compute
Gateway lets run it or read, tick or message its runs: any other token gets
403, even one holding the grant id or run id. app_id is both the Drive app
the module is published under and the Granite app the grant must be for.
Drive stores the capability list as JSON:
{"module": "main", "capabilities": ["console.print", "time.now"]}
What the module must export:
| Call | Export |
|---|---|
One-shot call (POST /v1/compute/invoke) | main: () -> i32; the return value is exit |
Durable run (POST /v1/compute/runs) | run_init: () -> i32 and run_step: () -> i32 |
| Nora interactive run | A module compiled from a Nora Program.interactive |
Modules that use any host function that reads or writes bytes must export
their linear memory as memory. 64-bit memories are refused.
Grants
Before running a module, Compute Gateway asks Granite whether the grant_id
you send:
- is active and not expired,
- covers the resource
compute:<app_id>, the module’s own app, - holds every capability the module’s list declares, and
- was granted to the module’s
app_id.
If not, the call fails with 400. When Granite answers “no”, the message
starts granite refused grant <id>: and gives Granite’s reason. Each durable-run tick and each Nora message
checks the grant again, so revoking it stops the next step.
The grant’s scopes are the capabilities the module may use: Compute
Gateway links only the host functions for those scopes. Its limits must
include per_call_fuel_cap, a whole number; a grant without it is refused.
In Granite, put these in the request’s requested_scopes and
proposed_limits, for example:
{"requested_scopes": ["console.print", "time.now"], "proposed_limits": {"per_call_fuel_cap": 10000000}}
Granite’s docs cover filing the request and the person’s approval.
Fuel and metering
Fuel counts the WebAssembly work a module does. For each call (and each step of a durable run), Compute Gateway:
- asks Spend Gateway to reserve up to the grant’s
per_call_fuel_capunits for the person and your app. If Spend Gateway refuses, the call fails with400and a message startingspend authorize; - runs the module with the reserved fuel as its limit;
- charges the fuel the module used, one unit per unit of fuel.
A module that traps or runs out of fuel is still charged the fuel it used. A module that fails before it starts (it doesn’t compile, or imports a host function its grant doesn’t allow) is charged nothing.
Only the module’s own work is metered here. Any cost of the services a capability calls is that service’s.
Spend Gateway decides whether to reserve; see its docs. Spend Gateway takes no payments.
Calls and runs
There are three ways to run a module.
One-shot call. POST /v1/compute/invoke runs main once and returns its
exit code, printed output and output bytes.
Durable run. POST /v1/compute/runs runs a module in steps, keeping its
state between them. Each step reads the state and the result of the previous
command, then emits one command:
| Command | Meaning |
|---|---|
{"cmd":"done"} | Finished, exit code 0. Emitting nothing also means done. |
{"cmd":"exit","code":3} | Finished with that exit code. |
{"cmd":"perform","cap":"drive.namespace.read","args":{"key":"notes"},"corr":"c1"} | Use a capability. corr is yours, echoed back. |
The command’s cap must be one the grant holds. Fast capabilities run within
the same request and feed the next step. A slow one (model.gateway.call,
pigeon.outbox.send) suspends the run: the response has
"status":"suspended" and pending_cap.
You drive the run. Compute Gateway never ticks a run on its own: call
POST /v1/compute/runs/{id}/tick to perform the command and run the next step,
and repeat until status is done or failed. A tick that is retried after
the command was performed does not perform it again. A suspended run you stop
ticking stays suspended, with no time limit, and you can tick it again later.
If a request is cut off partway through a run, reading the run shows it
suspended on the command it had reached, fast or slow, and a tick carries on
from there. A log append made just before the cut is not written twice.
It holds none of the owner’s funds while it waits: each step settles its spend
before the run suspends. If Spend Gateway is down when a step settles, the
request answers 503 but keeps the step’s result, and the run takes no
further step until a tick settles it; tick the run again, even if it reads
done or failed. Every tick checks the module and the grant again: if
the module is no longer in Drive, or the grant was revoked in the meantime, is
no longer known, or no longer covers the command the run is waiting on, the
tick fails with 400 and the run ends failed.
The next step reads the result as:
{"corr":"c1","ok":true,"result":{"bytes":"eyJtb2RlbCI6ImhhaWt1In0="}}
bytes is base64. On failure, ok is false and error holds a message.
When the run is done, its output is the final state. A failed run’s
response, and reading it later, carries error, the reason it failed.
Nora interactive run. POST /v1/nora/interactive-runs starts a Nora
interactive module, and POST /v1/nora/interactive-runs/{id}/messages sends it
one message. Compute Gateway keeps the module’s state between messages; each
response carries the rendered text and the module’s commands. Compute Gateway
does not carry out those commands.
Capabilities
| Capability | Host functions (module host) | What it does |
|---|---|---|
| (none needed) | get_input_len, get_input, post_output | Read the call’s payload; append to output |
| (none needed, runs only) | get_state_len, get_state, set_state, get_msg_len, get_msg, emit_cmd | Durable-run state and commands |
console.print | console_print(i32), console_say(ptr, len) | Print a number or a UTF-8 string to stdout |
time.now | time_now() -> i32 | The current Unix time in seconds |
drive.namespace.read | drive_read_len, drive_read | Read a file from the person’s space for your app in Drive |
drive.namespace.append_log | drive_log(ptr, len) | Append a line to that space’s log, the file log (one JSON {"message": ...} per line, with an "id" when a durable run wrote it, up to 256 KiB in all) |
model.gateway.call | model_call | Call a model through Model Gateway |
pigeon.outbox.send | pigeon_send(to, message) | Post into the person’s Pigeon thread to (a thread id they linked; see API examples) |
connector.call plus connector.call:<name> | connector_call | Call a connector the person registered; see API examples |
Functions that return bytes copy at most out_max bytes into the module’s
memory and return the number copied; call the _len function first to size
the buffer. In a durable run, use the capability as a perform command
instead of calling the host function.
Compute Gateway binds the person and your app to every capability call; the module cannot name a different person, app or Drive space.
Not yet:
drive.namespace.append_loghas been tested against a local Drive, not yet the live one.pigeon.outbox.sendhas been tested against a local Pigeon, not yet the live one.model.gateway.callhas been tested against a local Model Gateway, not yet the live one.
A module’s model.gateway.call runs under the person’s Model Gateway link,
not the grant the call or run names. The person links once; see
API examples. Until they do, every
model call fails without reaching Model Gateway.