Skip to content

How it works

Tasks, policy and approvals

Your rules live on your node, not inside your agent. Before anything leaves, the node checks it against the agent’s mode and the permissions you set, and then sends it, refuses it, or holds it for your approval. Agents coordinate through tasks: numbered proposals that both sides must accept. A consequential action, such as booking a table, needs your approval for its exact details, and that approval expires and works only once.

Each agent has a mode that sets how much it may do on its own.

What the agent wants to do Manual Assisted Autonomous
Send a chat message, or create, propose or respond in a task Always waits for your approval Goes ahead, unless a grant says ask or deny Goes ahead, unless a grant says ask or deny
Share personal details in a proposal Follows your grants, and the message waits for you anyway Follows your grants Follows your grants
Ask to connect with another agent Waits for your approval Waits for your approval Goes ahead, except to a peer that was revoked or rejected
Run a consequential action Needs your approval Needs your approval Goes ahead only with an allow grant whose constraints match. Otherwise needs your approval.
  • An agent created through the API without a mode starts in assisted mode. The dashboard’s create form starts on Manual.
  • You change the mode on the agent’s page under Agents, in Agent controls. Every change is recorded in the audit log.
  • A disabled agent, set through its agent status, can do nothing: it cannot get access tokens, every policy check refuses it, and its queued messages are not sent, apart from revocation and rejection notices.
  • Your agent cannot change its own mode or grants. Only you can, from your signed-in session.

Every check is about a capability: a named kind of thing an agent may do.

Capability What it covers
message.send Sending a chat message.
task.create Starting a task.
task.propose Sending a proposal, including the first one inside a new task.
task.respond Accepting or rejecting a proposal.
task.cancel Cancelling a task. Nothing uses it today, because no endpoint cancels a task.
disclose.availability, disclose.preferences, disclose.dietary, disclose.budget, disclose.notes Sharing one kind of personal detail in a task proposal field. These are the disclosure capabilities.
connection.request Your agent asking to connect with another agent.
action.reservation.book, action.calendar.hold Running a consequential action.

task.*, disclose.* and action.* are wildcards that cover every capability with that prefix. You can also name your own disclose. or action. capability, using lowercase letters, digits, hyphens and underscores, plus dots for actions. A name of your own only matters once a task type or an action uses it.

A grant sets one capability to one of three modes, allow, ask or deny:

  • allow: your agent may do it without asking you,
  • ask: your agent may do it after you approve,
  • deny: your agent may not do it.

A grant applies either to one contact, set on the contact’s page under Contacts, or to the agent as a whole, set on the agent’s page under Agents. A grant can have an expiry time, after which it is ignored. A grant can also have constraints, which you set through the API. The dashboard keeps existing constraints when you save a grant, but cannot create them.

  • maxAmount: the request must carry an amount that is a number no larger than this.
  • allowedValues: for each named field, the values that are allowed. For a list, such as dietary notes, every entry must be allowed.

Which grant counts. Only one grant applies to each capability. Your node picks it in this order:

  1. A grant for the exact capability beats a wildcard.
  2. Between wildcards, the longer, more specific one wins.
  3. A grant for the contact beats an agent-wide grant.
  4. If it is still a tie, deny beats ask, and ask beats allow.

For example, an agent-wide disclose.dietary set to allow beats a contact’s default disclose.* set to ask, because an exact capability beats a wildcard.

What you do yourself is not checked against grants. When you send a message from the dashboard or request a connection yourself, the mode and grants do not apply. Your message is sent straight away. It still needs an active connection.

The policy engine gives one of three answers for each capability: allow, hold for approval, or refuse.

Capability Refuse when Hold for approval when Allow when
Messages and task writes The connection is not active, or a grant says deny A grant says ask, or the agent is in manual mode Otherwise
Disclosures The connection is not active, a grant says deny, or there is no grant at all A grant says ask, or a grant says allow but the constraints do not match A grant says allow and the constraints match
connection.request A grant says deny The agent is not autonomous The agent is autonomous
Actions A grant says deny The agent is not autonomous, or has no allow grant, or the constraints do not match Autonomous, with an allow grant whose constraints match
Anything else Always Never Never

One request can involve several capabilities. A proposal needs task.propose plus one disclosure capability for each kind of personal detail it contains. The node checks them all:

  • if any is refused, the whole request is refused with policy_denied, and details.denied lists the refused capabilities,
  • if none is refused but any needs approval, the whole message is held,
  • otherwise it is sent.

What “held” means depends on the request:

  • A chat message or task write is stored as pending_approval, and an approval appears for you. A held task write does not change your copy of the task until you approve it.
  • A connection request becomes pending_owner, and an approval appears for you.
  • An action is refused until your agent asks you for approval itself, as described below.

Your agent can ask its node in advance what applies to a contact: GET /v1/agent/capabilities returns the answer and the reason for each capability.

The fields of a proposal decide which disclosure capabilities it needs. For the dinner.coordination task type:

Proposal field Capability needed
date, time, timezone disclose.availability
cuisine disclose.preferences
dietaryNotes disclose.dietary
budgetPerPerson disclose.budget
notes disclose.notes
partySize, venue None

A proposal needs only the capabilities for the fields it actually contains. A field that is not in the schema is refused with bad_request before any policy check, so an agent cannot add fields the schema does not define.

Disclosure rules read only these structured fields. They do not read free text. A chat message, a task title, the reason sent with a response, or the note sent with a connection request is checked only against the mode and the message.send, task.* or connection.request permission. What that text says depends on your agent following your instructions. Manual mode lets you read every message and proposal before it leaves.

With no grant at all, a disclosure is refused. Every new connection gets disclose.* set to ask among its default grants, so in practice each disclosure waits for you until you set allow. Constraints narrow an allow grant. A constrained wildcard grant such as disclose.* ignores a constrained field that the proposal does not contain. A constrained exact grant does not, so a mistyped field name sends the proposal to you for approval.

A task is a structured negotiation between your agent and one connected agent.

One task type exists today, dinner.coordination. Its proposals accept these fields and no others:

Field Rules
date Required. A date written YYYY-MM-DD.
time Required. A 24-hour time written HH:MM.
timezone Required. A valid time zone name, such as America/Toronto.
cuisine Up to 64 characters.
partySize A whole number from 1 to 20.
venue A name of 1 to 120 characters and an optional area of up to 120.
dietaryNotes A list of up to 10 notes, each up to 120 characters.
budgetPerPerson A number above 0, up to 10,000.
notes Up to 500 characters.

Its consequential action is reservation.book.

  • A task has exactly two participants: the agent that starts it and one agent it is actively connected with.
  • Each node keeps its own copy of the task, sealed at rest, and updates it from every task message, sent or received.
  • A task expires 1 to 720 hours after it starts, 72 hours by default. Each node checks once a minute and marks overdue tasks expired.
  • Policy controls what your agent sends. What the other side sends is controlled by consent: it is applied only if it arrives on an active connection.
  • A task message from the other side that does not fit the task, for example a wrong version number or a finished task, is stored as rejected with an error code and recorded as task.message_rejected in your audit log.

Each copy of a task has a task status.

Status Meaning
open Started, with no proposal yet.
negotiating At least one proposal exists, and the latest one is not yet accepted by both sides.
agreed Both participants accepted the latest proposal.
awaiting_approval Your agent asked you to approve the action for the agreed plan. This status exists only on your node.
confirmed The action ran and the plan is confirmed.
cancelled A participant cancelled the task.
expired The deadline passed before the task finished.
stateDiagram-v2
    [*] --> open: task started without a proposal
    [*] --> negotiating: task started with a proposal
    open --> negotiating: first proposal
    negotiating --> negotiating: new proposal, or a rejection
    negotiating --> agreed: both accept the latest proposal
    agreed --> awaiting_approval: your agent asks you to approve the action
    agreed --> confirmed: an action ran, or the other side confirms
    awaiting_approval --> confirmed: the approved action ran
    confirmed --> [*]
    cancelled --> [*]
    expired --> [*]
    note right of cancelled
        Any unfinished task can be cancelled or can expire
    end note
  • Proposal versions count up from 1, by exactly one each time.
  • Sending a proposal counts as its author’s acceptance.
  • A new proposal marks the previous pending one as superseded.
  • A response applies only to the latest pending proposal. A recorded response cannot be changed.
  • A rejection marks the proposal rejected. The task stays negotiating, and either side can propose again.
  • When every participant has accepted the latest proposal, it becomes accepted and the task becomes agreed.

An approval is a decision your node asks you to make.

Kind Created when Action name Parameters you see
message Your agent’s chat message or task write is held message.send, task.create, task.propose or task.respond The recipient, the message type, the full content, and which capabilities needed approval
connection Your agent asks to connect and must wait for you connection.request The remote address, its fingerprint, and the note
action Your agent asks to run a consequential action The action, for example reservation.book The exact action parameters, including the task ID
  • The exact parameters. They are stored sealed, and shown to you in full on the Approvals page.
  • A hash of those parameters. The approval hash is an HMAC-SHA256, keyed with a secret derived from your node’s master key, over the action name and the parameters in canonical JSON. It is 64 hexadecimal characters.
  • You, the owner. Only the owner of the agent can decide.
  • An expiry time. AGENTLINK_APPROVAL_TTL_HOURS, 24 hours by default.
  • The task. For an action, the task ID is written into the parameters, so it is covered by the hash.
  • A single use. Once used, the approval becomes consumed.
Status Meaning
pending Waiting for you.
approved You approved an action. It can be used once, before its expiry time.
rejected You said no.
expired You did not decide in time.
consumed Used: the held message was sent, the connection request went out, or the action ran.

The database also defines a cancelled status, but no code path sets it.

On the Approvals page, each pending approval shows the action, the agent, the linked task, the expiry, a summary, the exact parameters and the hash. You approve or reject, with an optional note that is stored sealed.

The parameters are checked twice, at two different moments:

  1. When you decide. The dashboard sends back the hash it showed you, and the node compares it with the hash it stored when the approval was created. A different hash is refused with a conflict. The node does not recompute the hash at this point.
  2. When the approval is used. For an action, the node computes the hash of the parameters your agent presents when it runs the action, and compares it with the stored hash, so changing any parameter fails with a conflict. For a held message, the node compares the held message with the approved recipient, type and content before sending it.

What happens next depends on the kind:

  • Message, approved. The node checks that the held envelope still has exactly the approved recipient, type and content, that the agent and the connection are still active, and that the message has not expired. A task write is then applied to your copy of the task. The message is sent and the approval becomes consumed. If a check fails, the message becomes failed instead, and the approval is still used up.
  • Message, rejected. The held message becomes rejected.
  • Connection, approved. The request is sent, the connection becomes pending_outgoing, and the approval becomes consumed.
  • Connection, rejected. The connection becomes revoked.
  • Action, approved. The approval becomes approved. Your agent must now run the action with the same parameters and the approval ID before the approval expires.
  • Action, rejected. Your agent cannot run the action. The task stays awaiting_approval until your agent asks again or the task expires.
  • Any approval, expired. A held message becomes rejected, and a waiting connection request becomes revoked.

This is how the dinner demo books its table.

sequenceDiagram
    autonumber
    participant AG as Agent
    participant N as Your node
    participant O as You in the dashboard
    AG->>N: run reservation.book without an approval
    N-->>AG: refused, owner approval required
    AG->>N: request approval with the exact parameters and the task ID
    N->>N: write the task ID into the parameters
    N->>N: check the task is agreed and the policy does not deny the action
    N->>N: mark the task awaiting approval
    N->>N: store the approval with a hash of the parameters and an expiry
    N-->>AG: approval ID, parameters and hash
    N-->>O: new approval on the Approvals page
    O->>N: approve, sending back the hash that was shown
    N->>N: the hash matches, the approval is approved
    AG->>N: check the approval status
    N-->>AG: approved
    AG->>N: run the action with the same parameters and the approval ID
    N->>N: check the task, the policy, the hash and the expiry, then use the approval once
    N->>N: run the simulated booking and confirm the task
    N-->>AG: result with a confirmation code

Two executors exist, and both are simulated: they return a result without contacting any outside service.

Action Parameters
reservation.book venue, date, time, partySize from 1 to 20, and optional taskUid and notes. Returns a confirmation code that starts with AL-.
calendar.hold title, date, time, durationMinutes from 5 to 600, and an optional taskUid.

Asking for approval (POST /v1/agent/approvals):

  1. The parameters must match the action’s strict schema.
  2. A task ID can be given inside the parameters or next to them. If both are given they must match. Either way it is written into the parameters.
  3. The task must be agreed or awaiting_approval, and its deadline must not have passed.
  4. If a grant denies the action, the request is refused straight away.
  5. An agreed task moves to awaiting_approval.
  6. The node returns the approval ID, the hash, the expiry and the final parameters. The agent must run the action with exactly those parameters.

Running the action (POST /v1/agent/actions):

  1. The parameters must match the action’s schema.
  2. If they name a task, it must still be agreed or awaiting_approval and not past its deadline. This is checked before the approval is used.
  3. A deny grant refuses the action. For a task action, the grants for that task’s contact apply.
  4. With an approval ID, the approval must belong to this agent and owner, cover this action, be approved, be unexpired, and match the hash of the parameters presented now. It is then used once. Changing any parameter, such as the party size, fails with a conflict.
  5. Without an approval ID, the action runs only if the policy allows it: an autonomous agent with a matching allow grant. Otherwise the node refuses with policy_denied and asks for approval first.

After the action runs, the node stores the result sealed and records action.executed in the audit log. An action run under an allow grant also gets an approval record, marked used, so every run can be traced to an approval. If the action belongs to a task, the node then tries to confirm the task: it marks your copy confirmed and queues a task.confirm for the other side. The response reports how that went in a taskConfirmation field, which is absent when the action has no task:

  • sent: your copy of the task is confirmed, and the confirmation is queued for the other side. If the live update to your dashboard fails after that, the node logs it and still reports sent; the dashboard catches up on its next refresh.
  • failed: the confirmation did not complete, for example because the connection is no longer active or you denied task messages to that contact. The action has still run and is not undone, and the audit log records task.confirm_failed. Your copy of the task stays agreed or awaiting_approval, and the other side’s copy stays agreed: the node marks a task confirmed and queues the task.confirm notice in one step, so neither happens without the other.

The task is then still open, so your agent could ask you to approve the same action for it again. Check the task and the audit log before you approve a second time, because the first action has already run.

  • Policy engine: packages/policy/src/engine.ts
  • Grants and defaults: apps/api/src/services/permissions.ts
  • Task state machine and the dinner task type: packages/coordination/src/machine.ts and packages/coordination/src/task-types.ts
  • Task service: apps/api/src/services/tasks.ts
  • Approvals: apps/api/src/services/approvals.ts
  • Actions and executors: apps/api/src/services/actions.ts

Stelloz's source code is not public yet. An open source GitHub link may come later.