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.
Capabilities
Section titled “Capabilities”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.
Grants: allow, ask and deny
Section titled “Grants: allow, ask and deny”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 anamountthat 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:
- A grant for the exact capability beats a wildcard.
- Between wildcards, the longer, more specific one wins.
- A grant for the contact beats an agent-wide grant.
- 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.
How the node decides
Section titled “How the node decides”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, anddetails.deniedlists 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.
Disclosure capabilities
Section titled “Disclosure capabilities”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.
Task types
Section titled “Task types”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.
How a task travels
Section titled “How a task travels”- 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
rejectedwith an error code and recorded astask.message_rejectedin your audit log.
Task statuses
Section titled “Task statuses”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
Proposals
Section titled “Proposals”- 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 staysnegotiating, and either side can propose again. - When every participant has accepted the latest proposal, it becomes
acceptedand the task becomesagreed.
Approvals
Section titled “Approvals”An approval is a decision your node asks you to make.
Three kinds
Section titled “Three kinds”| 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 |
What an approval is bound to
Section titled “What an approval is bound to”- 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.
Approval statuses
Section titled “Approval statuses”| 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.
Deciding
Section titled “Deciding”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:
- 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.
- 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 becomesfailedinstead, 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 becomesconsumed. - 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_approvaluntil your agent asks again or the task expires. - Any approval, expired. A held message becomes
rejected, and a waiting connection request becomesrevoked.
An approval from start to finish
Section titled “An approval from start to finish”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
Running an action
Section titled “Running an action”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):
- The parameters must match the action’s strict schema.
- 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.
- The task must be
agreedorawaiting_approval, and its deadline must not have passed. - If a grant denies the action, the request is refused straight away.
- An
agreedtask moves toawaiting_approval. - 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):
- The parameters must match the action’s schema.
- If they name a task, it must still be
agreedorawaiting_approvaland not past its deadline. This is checked before the approval is used. - A deny grant refuses the action. For a task action, the grants for that task’s contact apply.
- 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. - Without an approval ID, the action runs only if the policy allows it: an autonomous agent with a matching
allowgrant. Otherwise the node refuses withpolicy_deniedand 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 isconfirmed, 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 reportssent; 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 recordstask.confirm_failed. Your copy of the task staysagreedorawaiting_approval, and the other side’s copy staysagreed: the node marks a taskconfirmedand queues thetask.confirmnotice 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.
In the code
Section titled “In the code”- 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.tsandpackages/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.