The operation contract
An agent can read and edit files in its sandbox freely. Anything that touches a system outside the sandbox (a Terraform apply, a database restore, a posted message, a merged pull request) goes through one path: the operation contract. The agent proposes an exact operation, the control plane records it, a person or a standing rule approves exactly that, a runner executes it, and a separate read checks what happened.
The user-facing view is on Approvals and guardrails. This page is the code view. Everything here lives in api/src/modules/operations/.
The types
Section titled “The types”| Type | File | What it is |
|---|---|---|
Capability |
domain/operations.ts |
{ id, mode, input_schema }. mode is read or write. |
Target |
domain/operations.ts |
One thing a connection can act on: resource_id, environment, service, repository. |
NormalizedOperation |
domain/operations.ts |
The provider’s canonical form of a request: resource_id, resource_key, arguments. |
Operation |
application/ports.ts |
The stored request, with its fingerprint, binding, status, review_target, result and verification. |
OperationProvider |
application/ports.ts |
The port every integration implements: normalize, permits, read, review, dispatch, verify. |
DispatchResult |
domain/operations.ts |
What dispatch returns: status running, failed or unknown. |
Verification |
domain/operations.ts |
What verify returns: status verified, pending or not_observed, plus what was observed. |
An operation’s status is one of awaiting_approval, running, succeeded, failed, unknown, rejected, canceled, expired (OperationStatus).
The worker commands
Section titled “The worker commands”The runtime reaches this module only through POST /api/internal/v1/operations/commands. The body is one of six kinds (operationCommandSchema in application/contracts.ts):
| Kind | Purpose |
|---|---|
list_operation_capabilities |
The connections, targets and capabilities this run’s grants cover |
describe_operation_target |
The input schema for one capability on one target |
read_operation |
Execute a read now and return a receipt |
propose_operation |
Review a write and record it for approval |
get_operation |
Read a request’s current state |
verify_operation |
Run the verifying read |
Every command carries command_id, tenant_id, task_id, run_id, attempt_fence and expected_run_version. The command is stored with a digest of its whole body. Repeating it returns the stored response; reusing the ID with a different body is refused.
1. Propose
Section titled “1. Propose”OperationsService.prepare handles propose_operation in three steps.
Check. The capability must exist on the connection with mode write. The provider normalizes the arguments. The run’s current grants must cover that capability on that target, and the connection’s registered scope must permit it. An agent cannot propose against a target it was not granted.
Review. Outside any transaction, the service calls provider.review(...). For a runner-backed provider this is a signed request to the runner, using the runner’s read identity. Nothing is changed. The answer is evidence of exactly what would happen: for Terraform, a saved plan with its plan_id, plan_sha256, the commit it was made from and when it expires.
Record. The operation is inserted with status awaiting_approval. The review evidence is stored as review_target, and two digests are computed:
binding = operationDigest([provider, connection.id, endpoint, scope, credential])fingerprint = operationDigest([tenant_id, task_id, run_id, id, binding, capability_id, normalized, evidence])operationDigest is SHA-256 over canonical JSON (keys sorted). The response returned to the agent is stored as the command’s idempotent answer.
2. What an approval is bound to
Section titled “2. What an approval is bound to”An approval is not “yes to this task”. It is bound to five things, and a change in any of them invalidates it.
| Bound to | How |
|---|---|
| The operation | The fingerprint covers the task, the run, the operation ID and the capability. The approve and reject routes require the caller to send the fingerprint they reviewed (decisionSchema); a mismatch is refused. |
| The target | The binding covers the connection’s provider, endpoint, registered scope and sealed credential. If an administrator edits or replaces the connection, the binding no longer matches and the request must be proposed again. |
| The arguments | The normalized arguments are inside the fingerprint. |
| A source revision or digest | The arguments of a plan-bound capability name what will be applied. terraform.apply requires commit, plan_id and plan_sha256. CloudFormation binds a change set and its digest, an image publish binds archive and manifest digests, an MCP call binds the tool’s schema digest. |
| An expiry | A request waits for ttl_hours of its approval rule, or the deployment default POLO_API_APPROVAL_TTL_HOURS. After that expireIfDue sets it to expired. The reviewed plan has its own expiry on the runner. |
Who may approve, how many approvals are needed (one or two) and whether the requester may approve their own change come from the workspace approval policy (domain/approval-policy.ts), chosen by the operation’s environment.
Approve
Section titled “Approve”OperationsService.decide does not trust the review it stored. When an approval completes the required count, it:
- calls
provider.review(...)again and compares its digest with the storedreview_target. If the runner now reports something different, the approval fails with “The runner review changed. Request a new reviewed operation”; - re-checks, under a row lock, the fingerprint, the expiry, the policy, the approver’s role, and that the task and the requesting run are still running and still hold the grant;
- takes a lock on the target’s
resource_keyand refuses if another operation on that target is still unresolved (“Another operation on this target needs verification first”); - sets the status to
running, records who approved and when, and appends the decision to the decision log.
Only then, after that transaction commits, does it dispatch.
Pre-authorized actions
Section titled “Pre-authorized actions”An administrator can create a standing approval for one exact operation (domain/preauthorization.ts): an exact environment, connection, capability, resource and arguments, with a daily limit and an expiry. A proposal that equals the rule on every field moves straight to running. A provider must opt in with preauthorizable; today only mcp.call does. Plan-bound and digest-bound changes cannot be pre-authorized, because what they apply is not known in advance.
3. Execute through a runner
Section titled “3. Execute through a runner”provider.dispatch(connection, operation) sends the approved request. For a customer runner this is an HTTPS PUT to a path that ends in the operation ID, signed with the connection’s key:
signature = HMAC-SHA256(key, "PUT\n" + path + "\n" + timestamp + "\n" + body)headers: X-Polo-Timestamp, X-Polo-SignatureThe runner verifies the signature and rejects a timestamp more than 60 seconds off (signed_body in tools/common/runner_support.py). It then checks the request against its own configuration. The Terraform runner, for example, looks up the saved plan and requires the task, run, stack, commit and plan digest to match before it applies (_plan_row in tools/terraform/runner.py). The runner, not Polo, holds the cloud credentials, and it uses a separate identity for applying than for reading.
Because the operation ID is in the path, sending the same request twice does not apply twice. The runner returns the receipt it already has.
4. Receipts
Section titled “4. Receipts”A runner answers with a receipt: the request’s id, the task_id and run_id, a status, a result, and a bounded reason code when it failed. Runners keep their receipts, so a later question about an old request can be answered from the runner’s own record.
On Polo’s side, dispatchRunning stores the outcome on the operation as result and error. A read returns its receipt directly to the agent as { id, connection_id, status: 'succeeded', result }.
5. The separate verifying read
Section titled “5. The separate verifying read”A successful dispatch is not success. The operation becomes succeeded only after provider.verify(...) returns verified.
Verification is a different read from the write:
- Terraform makes a refreshed plan after the apply and requires it to report no changes, then checks the state (
_converged). - MCP calls a different, read-only tool that was granted as the write tool’s verifier, and compares one path of its result with an expected value that was part of the approved arguments.
- Cloud machine changes read the provider’s record of the request and the resource again (
tools/common/cloud_change.py).
The agent triggers verification with verify_operation; a person can trigger it from the task view, which calls POST /api/operations/requests/:id/verify. The result maps to a status like this:
Verification |
Operation becomes |
|---|---|
verified |
succeeded, with result set to what was observed |
rejected_before_dispatch set |
failed: the runner refused before changing anything |
pending |
stays running |
not_observed, more than 120 seconds after approval |
unknown |
6. Unknown outcomes and reconciliation
Section titled “6. Unknown outcomes and reconciliation”Sometimes Polo cannot tell whether a change happened: the connection dropped after the request was sent, or the runner never confirmed. The contract has a status for exactly this, and it is the part to preserve when you change this code.
- If
dispatchthrows, the operation is recorded asunknownwith the error “Dispatch could not be confirmed. Verify the existing receipt before another change.” - For providers that set
requiresDispatchReceipt, arunningoperation with no receipt after 120 seconds becomesunknown: “The mutation has no dispatch receipt. It remains unresolved and will not be replayed.” - An unknown operation is never retried automatically. Temporal may retry an activity, but the activity only polls and verifies. The mutation itself is sent once.
- An unknown operation blocks its target. The resource lock in step 3 of approval refuses a new change to the same target until this one is resolved.
- Reconciliation is verification. Running the verifying read again settles it:
verifiedmakes itsucceeded, a runner receipt that says it was refused makes itfailed. Until then it staysunknown, and the UI says “Outcome unknown · Polo couldn’t confirm that it ran”. - If the connection changed in the meantime, verification refuses with “The operation connection changed. The receipt still requires reconciliation”, because a different endpoint or credential could not speak for the original request.
The same idea appears on the runner side. The cloud change kit writes down what it is about to send before sending it, so a request it has no record of provably asked nothing of the provider.
Where to go next
Section titled “Where to go next”- Tools and runners shows how to add a provider and a runner that follow this contract.
- States lists the operation statuses and the outcome words shown in the app.
- Runners and vault is the user’s view of runners.