Documentation
Guides and reference for OutLayer verifiable compute and agent custody.
Docs navigation
Tasks
An agent prepares; the owner reads and approves; the agent's run carries it out. A run of a project, admitted to an owner's secret row, leaves that owner a task through the outlayer:tasks host interface. The owner reads it in their inbox with no run, and approves it with one message their wallet signs; the platform then starts a run of the agent that prepared it — on the agent's own payment key, within the compute limit of the run that prepared it — which executes the operation the task names. Any project can use it, connectors among them.
What a task is#
An action prepared and waiting for its owner. “Confirm this email”, “show me the bet before it is placed”, “give me your photo”. The agent's call leaves it; the owner signs; a run of the agent, started by the platform, carries it out; the agent learns the outcome the next time it asks.
- Not a paused run. Nothing waits inside the enclave. A task is a record.
- Not a run of the owner's. The owner sends no transaction and pays nothing. The run that acts is the agent's, paid by the agent's key, and bounded by the run that prepared it.
- Not a script the owner is made to run. A task names the operation that answers it and carries no arguments for it. The project's code decides what happens.
- Not a replacement for a service's own confirmation. A bank's approval rules stay where they are.
Notices#
A task asks yes or no (confirm), or for something the owner supplies (input) — or nothing at all. A notice (notice) tells the owner that something happened; they read it in the same inbox and press Got it. It names no operation and no run follows it, so it needs no payment key: a run on chain may open one. It is shown, sealed and limited as a task is, and the agent reads it open until it is seen, then done.
…
Connectors that ask first#
A connector can hold a write until its owner approves it. The owner chooses which writes, and the default is none: a policy that names none acts at once, as it always did.
| Connector | What can wait for the owner |
|---|---|
| Gmail | send |
| GitHub | every write: branch_create, file_put, commit, issue_create, issue_comment, issue_update, pr_create, pr_review, pr_merge, gist_create, gist_update, repo_star, repo_unstar |
| Mercury | pay_invoice, add_recipient, send_invoice, cancel_invoice — by rule, with conditions: the amount (min_usd, max_usd), the payment rail (methods), a saved or a new payee (payee) |
Turning it on. On the connector's page — Gmail, GitHub — the policy editor has a group “Ask me before”: one box per write. What is ticked is stored in the policy as confirm:
…
Mercury decides per call instead: its policy's rules are a list of {when, then}, read in order, and the first rule whose when matches decides — allow, ask or refuse. A payment above $500, every wire, every cancellation can wait while a small ACH payment to a saved payee runs at once. A rule never widens the policy: it is asked last, of a write every other check passed.
A name that is not one of the connector's writes — a read, a misspelling, another case — makes the policy unreadable, and an unreadable policy refuses the connector's writes. Asking before a write allows nothing by itself: on GitHub the write must also be in actions.
What the agent gets. Its call is checked exactly as it would be before the write — every rule of the policy — and instead of writing, the connector leaves a task for the owner and answers:
…
That is a success: the agent did its part and paid the operation's price. It learns the outcome with task_status and the task_id: done with the write's result, rejected with the owner's reason if they gave one, or failed, expired, void.
What the owner sees. The task waits in the inbox as the prepared form, in three views: Preview — every value the write will use, the ones the agent wrote marked 🤖; RAW form — the exact bytes the task's hash covers; RAW request — what the agent's run was called with. Beside it, the proof that a published build of the connector made the task, with the run's attestation on request. What a field cannot hold — a Gmail attachment, a file's content on GitHub — is handed over as a download. Approving is one message the owner's wallet signs, with a note for the agent if they want to give one: no transaction. The platform then runs the connector's confirm as the agent, on the agent's own payment key, within the compute limit of the call that prepared the task; confirm is priced at zero.
Carried out exactly as shown. confirm acts on the write sealed in the task and on nothing else: it takes no value from its own call and re-reads nothing the write was built from. It checks the policy and the day's count again as they are at that moment; a policy changed since the task was made makes the task void. On GitHub the owner's yes is bound to the commit they were shown: a merge is made with that head commit, so GitHub refuses it if the pull request moved, and an approval of any other head is refused. A confirmed write counts against the owner's daily number for the agent that prepared it, beside the agent's direct writes.
Saying no. Reject in the inbox takes an optional reason, encrypted in the page for the connector; nothing is written, and the agent reads the reason with task_status the next time it asks. A write refused after the owner's approval ends the task failed, with the reason, and the agent's sentence says whether the write happened, so a write that was made is not prepared twice.
How a connector builds this is in CONNECTOR_TASKS.md.
Using it in a project#
Declare it in the manifest, and build on the SDK with the feature:
…
…
Where the owner must be asked, open a task and answer the agent awaiting_owner:
…
…
Write the operation the task names. The platform runs it as the agent on the owner's approval; it takes the answer, acts, and reports:
…
The rest is the same in every project, and the SDK answers it:
…
| Operation | Who | Answers |
|---|---|---|
task_status | preparer | where one of its tasks stands, with the result or the reason |
tasks | preparer | its tasks for this owner; {"tasks": []} when there are none |
task_cancel | preparer | withdraws an open task |
task_delete | preparer | deletes a task, in any state |
tasks_unlock | owner | writes the copies of the waiting tasks for the devices now signed in |
Three rules a project keeps: check the request against the policy before the task is made; check it again, as of that moment, before acting; and show what will be done whole — what cannot be shown whole is refused, never shown in part.
A task is paid for when it is prepared. In a connector, the operation that opens a task keeps its price, and nothing comes back if the owner says no. The operation that answers a task, and the five above, are priced at zero: an owner pays nothing to say yes, and the run the platform starts for an approval is admitted only when its operation is priced zero. Its compute is the agent's to pay, as any call of the agent's is, up to the compute limit of the run that prepared the task.
Conversations#
A task may ask the owner for text or a file instead of a yes, and the run that takes the answer may ask the next question at once. The new task is the next turn of the same thread: it keeps the conversation's preparer, so the inbox shows it from the same agent, and it counts in that agent's share and under its mute.
…
An example: a guessing game. connector-probe plays one. The agent calls guess_start; the run picks a number from 1 to max and leaves the owner a task, “Guess my number”, answered awaiting_owner:
…
- The owner types a guess in the inbox and approves; the platform runs
guessas the agent, which judges it. - Wrong:
higherorlowerarrives as the next task of the same thread, showing the guess, the answer and the attempts so far. Right: the game ends with a notice in the thread, “You guessed it: 57, in 6 attempts”, which asks nothing. - The agent follows it with
task_status: each turn's result is{attempt, guess, verdict, max, detail}, withnext_task_idwhile the game goes on andnotice_task_idwhen it is won. - The number is in the task's sealed
state, handed from turn to turn, beside random bytes: the owner holds the state's hash, and without them hashing every number would name it.
Whose task, and who may do what#
The owner of a task is the owner of the secret row the run named and the keystore opened. The preparer is the account that made the run. Both are the platform's facts: no function of the interface takes an account.
| The run is | It may |
|---|---|
| the project's, made by an account the owner's row admits by name | open a task for that owner; read, cancel and delete its tasks: those it made and the turns of its conversations |
| the one the platform started for an approved task: the preparer's, on the preparer's key | answer that task, report its result, and open the next turn of its conversation |
| the project's, made by the owner | open tasks for themselves; open their waiting tasks for a new device |
| admitted by a row open to everyone, to a pattern, or to holders of a token or a role | run; open is refused not-granted-by-name |
| another agent of the same owner | nothing of the first agent's tasks |
| another project's | nothing of this project's tasks |
| one that names no row, or whose row did not open | nothing: no-owner |
| one a contract relayed: the account that called OutLayer is not the account that signed | nothing: relayed |
A task is opened over HTTPS only: the consent to carry out the owner's answer is a payment key, the one that pays for the run that carries it out. No key answers a task by calling: a key of the owner's account is not the preparer, and the preparer's own call carries no approval. The run that answers is started by the platform and by nothing else.
As with a bot in a messenger, nobody writes to a person who has not let them, and the owner silences whom they please: they mute an agent or a project in the inbox, and delete its waiting tasks at once. Who is muted, the devices signed in and the URL task events go to are on the inbox's settings screen, where a mute is lifted and a device withdrawn.
What the owner is shown#
A title of 80 characters and up to 12 fields. Each field is a label, one of six kinds, its values, and whose words they are: the project's or the agent's.
| Kind | Holds | At most |
|---|---|---|
money | an amount with its unit | 200 characters |
account | an account at a service or on a chain | 200 characters |
address | an address: of mail, of a wallet | 200 characters |
text | one line | 500 characters |
long_text | text with line breaks | 50000 characters |
list | 1 to 20 values | 200 characters each |
Every value is drawn as plain text. No markup is interpreted, no link can be pressed, no image is loaded. Control characters, characters of zero width and characters that reorder text refuse the task. The host checks all of it, whatever the project was built with.
A task may carry files — an attachment of a message, a document — up to ten, 6 MiB together. The owner's page lists each by name, type and size and hands it over as a download; it holds the bytes it opened to the size and the hash the task names, and draws no file in itself. The operation that answers the task gets the files back.
States#
| State | Means |
|---|---|
open | waits for the owner |
approved | the owner approved; the platform queued the agent's run, named in run, which has not taken the answer yet |
answering | that run took the answer and acts |
done | that run ended well and the project reported; for a notice, the owner pressed Got it |
failed | the run could not be started, did not start, refused the task, or ended any other way; failure_reason says which: preparer_key_unavailable, operation_priced, operation_unknown, operation_limit_reached, wallet_unresolved, queue_unavailable, run_not_started, or run_refused:<reason> with the host's reason before the run took the answer; after it, run_failed (the project said it did not carry the task out; result holds why, a connector's refusal as {"error": …}), run_trapped (it reported the task carried out, then the run failed; result holds the report), run_unreported (the run ended without a word on the task: it may have acted) or run_unfinished (no word of its end in time) |
rejected | the owner said no, with a reason if they gave one |
cancelled | the preparer withdrew it |
expired | past its life: 24 hours at most |
void | the policy changed since it was made |
Each move is made once, and a task never returns to open: a run that failed may have acted in part. A task that leaves open loses what it showed at once; its outcome is kept 30 days. A task approved for thirty minutes without a run taking its answer is ended failed by the platform. A notice is open, then done, cancelled or expired, and counts in every limit below as a task does.
| Limit | Value |
|---|---|
| open tasks addressed to one owner | 20 |
| of them, from one preparer, open and approved together | 10 |
| tasks one run opens | 5 |
| what the waiting tasks of one owner hold together | 64 MiB |
| files of one task | 10, and 6 MiB together |
the project's state | 256 KiB |
| the task, without its state and files | 256 KiB |
What is sealed, and who reads it#
The platform stores a task and opens none of it. What a task shows is kept twice: sealed under a key that exists only in the enclave, and under a content key encrypted to each device the owner signed in. A device is a convenience, never the only copy.
…
| Whoever holds | Can | Cannot |
|---|---|---|
| the platform's database | see who was asked by whom, of what kind and when; delete a task | read what a task shows; add a device of their own; act on a task |
| a session's token | list tasks as ciphertext; reject and delete | read a task without the device's key; approve a task |
| the agent's payment key | prepare tasks, and pay for the run that carries an approved one out | approve a task, or answer one by calling |
The proof#
Before the inbox offers the button, it checks that the task was made by a published build of its project, and shows the run's attestation on request:
- the run that made the task is attested, by an enclave whose measurements are approved on chain;
- it was a run of the task's project, made by the account the task names;
- the build that ran is a version of that project on the contract;
- what the run answered hashes to the hash the enclave signed for;
- that answer names the task with the hash of exactly what the page opened.
So a project's answer has to name the task it opened: tasks::awaiting_owner is that, put wherever the answer has room for it. A task written into the platform's database can be shown and cannot be acted on: the project acts on the task sealed in the enclave, and the owner's approval must name its hash. The run that carried a task out is named by run once there is one, and its attestation is public too: the page holds it to the task — a call of the agent, of the task's project, of the build the task names.
Being told#
The owner sees every task in the inbox, on any device they signed in. To hear of it elsewhere they name a URL of their own on the inbox's settings — an HTTPS URL on a public host, with no credentials in it — with one signature of their wallet. The URL stays in force after the session that named it ends.
It is sent task_created, task_approved (with the run queued for it), task_answered, task_failed (with its failure_reason) and task_expired; a notice arrives as task_created with "kind": "notice", and Got it sends nothing. A body says who asked whom, of what kind and when, and links to the inbox. It carries nothing of what the task shows: what the owner is shown is sealed for their devices, and a URL is not one of them.
Each request carries X-Wallet-Id: owner:<account> and X-Webhook-Signature, the HMAC-SHA256 of the body in hex under a secret made when the URL was named and shown to the owner once; naming a URL again makes a new one. The sender follows no redirect and connects only to a public address. The agent is told nothing: it learns a task's outcome from task_status the next time it asks.
The owner's session#
The owner signs one message with their wallet (NEP-413, recipient: the OutLayer contract). It moves nothing and approves nothing:
…
- The device key is the public half of a key pair the browser made and keeps non-extractable: the page uses it and cannot read it.
- The session lasts until the deadline in the sentence, 30 days at most, and is not extended.
- One statement opens one session. An account has five devices in force at most, each with a session and a key of its own; one more retires the device signed in longest ago, which then says so. The owner sees their devices in the inbox's settings and withdraws any of them.
- Before a task is encrypted to a device, the enclave checks the statement itself: the account, the deadline, the signature, and that the key that signed is a full-access key of the account on chain. An implicit account that no transfer has made yet is signed for by its own key, the one it will be made with.
- A session lasts while the key that signed it is a full-access key of the account: a key removed from the account ends the session it opened.
- A device lost together with the wallet key that signed it in is cut off for certain by removing that key from the account.
Without a session nothing is told, not a count. A task made before a device signed in opens there after one call of the project's tasks_unlock by the owner.
The approval#
Approving a task is the same kind of signature over the task, made from the owner's click, good for ten minutes, once:
…
- The hash is the SHA-256 of the task's bytes as the page opened them: what the owner read is what the enclave must hold.
- The supply is the SHA-256 of
{"note":<base64|null>,"supplied":<base64|null>}over what the owner wrote — their answer to aninputtask, and a note for the agent beside any approval — each sealed by the page to the task's reply key. The owner's words are under the owner's signature, not merely beside it. - The coordinator rebuilds the sentence from the session's account, the task's id and the request, verifies it, spends the nonce, moves the task to
approvedand queues the agent's run. The enclave rebuilds it again from the sealed task and the run's input, and takes an approval up to ten minutes ahead of its clock and up to thirty behind. - The run is admitted as the agent's own call: the agent's payment key, wallet and identity binding as the preparing run had them, and no more compute than that run was allowed. A run that cannot be started ends the task
failedat once, with the reason.
Refusals#
A project answers a refusal as {"success": false, "error": "<code>: <sentence>"}. Nothing is never a refusal, and a refusal is never an empty list.
| Code | When | Retry |
|---|---|---|
not_granted_by_name | the row admitted the run by a rule that does not list the caller | no |
no_owner | no row named, the row did not open, or no project | fix the call |
relayed | a contract made the run: the account that called OutLayer is not the account that signed | call OutLayer directly |
muted | the owner muted this agent or this project | no |
inbox_full | a limit of open tasks | when the owner answers some |
task_run_limit | this call opened or asked as much as one call may | in another call |
display_invalid | what is shown is outside the bounds; the sentence names what | fix the request |
task_too_large | the state, the task or a result is over its bound | make it smaller |
task_life_too_long | a life asked beyond 24 hours | ask for less |
task_no_payment_key | a task opened from a run on chain: a task is opened over HTTPS, with the key that pays for the run that carries it out | call over HTTPS |
task_not_found | no such task of this project, owner and preparer | no |
not_the_owner | tasks_unlock in another account's call | no |
not_the_preparer | the answering operation in a run that is not the one the platform started for the task | no |
task_approval_invalid | the owner's signature does not verify over this task, this hash and these words, or the task was never approved | no |
task_hash_mismatch | the hash named is not the task's | no |
task_answer_invalid | another operation than the task names, or what was supplied is not what was asked | no |
task_closed | approved, answered, rejected or cancelled already | no |
task_expired | past its life | prepare it again |
task_void | the policy changed, or another build answered | prepare it again |
task_unreadable | a sealed copy that does not open | no |
task_store_unavailable | the store or the chain did not answer | yes, later |
task_internal_error | a fault of the platform that a repeat does not mend | no |
Reference#
- TASKS.md — the model, the inbox API, the events
- tasks.wit — the host interface
- tasks-probe — a project that does nothing else
- CONNECTOR_MANIFEST.md — the manifest reference, section
tasks - CONNECTOR_TASKS.md — owner confirmation in a connector