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.

rust
…

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.

ConnectorWhat can wait for the owner
Gmailsend
GitHubevery 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
Mercurypay_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:

json
…

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:

json
…

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:

json
…
toml
…

Where the owner must be asked, open a task and answer the agent awaiting_owner:

rust
…
json
…

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:

rust
…

The rest is the same in every project, and the SDK answers it:

rust
…
OperationWhoAnswers
task_statuspreparerwhere one of its tasks stands, with the result or the reason
taskspreparerits tasks for this owner; {"tasks": []} when there are none
task_cancelpreparerwithdraws an open task
task_deletepreparerdeletes a task, in any state
tasks_unlockownerwrites 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.

rust
…

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:

json
…
  • The owner types a guess in the inbox and approves; the platform runs guess as the agent, which judges it.
  • Wrong: higher or lower arrives 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}, with next_task_id while the game goes on and notice_task_id when 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 isIt may
the project's, made by an account the owner's row admits by nameopen 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 keyanswer that task, report its result, and open the next turn of its conversation
the project's, made by the owneropen 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 rolerun; open is refused not-granted-by-name
another agent of the same ownernothing of the first agent's tasks
another project'snothing of this project's tasks
one that names no row, or whose row did not opennothing: no-owner
one a contract relayed: the account that called OutLayer is not the account that signednothing: 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.

KindHoldsAt most
moneyan amount with its unit200 characters
accountan account at a service or on a chain200 characters
addressan address: of mail, of a wallet200 characters
textone line500 characters
long_texttext with line breaks50000 characters
list1 to 20 values200 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#

StateMeans
openwaits for the owner
approvedthe owner approved; the platform queued the agent's run, named in run, which has not taken the answer yet
answeringthat run took the answer and acts
donethat run ended well and the project reported; for a notice, the owner pressed Got it
failedthe 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)
rejectedthe owner said no, with a reason if they gave one
cancelledthe preparer withdrew it
expiredpast its life: 24 hours at most
voidthe 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.

LimitValue
open tasks addressed to one owner20
of them, from one preparer, open and approved together10
tasks one run opens5
what the waiting tasks of one owner hold together64 MiB
files of one task10, and 6 MiB together
the project's state256 KiB
the task, without its state and files256 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.

text
…
Whoever holdsCanCannot
the platform's databasesee who was asked by whom, of what kind and when; delete a taskread what a task shows; add a device of their own; act on a task
a session's tokenlist tasks as ciphertext; reject and deleteread a task without the device's key; approve a task
the agent's payment keyprepare tasks, and pay for the run that carries an approved one outapprove 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:

text
…
  • 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:

text
…
  • 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 an input task, 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 approved and 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 failed at 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.

CodeWhenRetry
not_granted_by_namethe row admitted the run by a rule that does not list the callerno
no_ownerno row named, the row did not open, or no projectfix the call
relayeda contract made the run: the account that called OutLayer is not the account that signedcall OutLayer directly
mutedthe owner muted this agent or this projectno
inbox_fulla limit of open taskswhen the owner answers some
task_run_limitthis call opened or asked as much as one call mayin another call
display_invalidwhat is shown is outside the bounds; the sentence names whatfix the request
task_too_largethe state, the task or a result is over its boundmake it smaller
task_life_too_longa life asked beyond 24 hoursask for less
task_no_payment_keya task opened from a run on chain: a task is opened over HTTPS, with the key that pays for the run that carries it outcall over HTTPS
task_not_foundno such task of this project, owner and preparerno
not_the_ownertasks_unlock in another account's callno
not_the_preparerthe answering operation in a run that is not the one the platform started for the taskno
task_approval_invalidthe owner's signature does not verify over this task, this hash and these words, or the task was never approvedno
task_hash_mismatchthe hash named is not the task'sno
task_answer_invalidanother operation than the task names, or what was supplied is not what was askedno
task_closedapproved, answered, rejected or cancelled alreadyno
task_expiredpast its lifeprepare it again
task_voidthe policy changed, or another build answeredprepare it again
task_unreadablea sealed copy that does not openno
task_store_unavailablethe store or the chain did not answeryes, later
task_internal_errora fault of the platform that a repeat does not mendno

Reference#