Skip to content

Tools, policies and approvals

Locus separates a tool’s description from permission to execute it. Publishing a tool makes an immutable version available for review. Execution also requires an enabled version, a current grant, a funded budget and a passing policy decision.

For a refund tool, the input schema might require an order ID and amount. Its effect is a write. The policy can require two reviewers before the action reaches the refund endpoint. A model cannot grant itself that permission by changing its output.

Register the REST or MCP destination with createToolEndpoint, then verify the endpoint using its returned ownership instructions. An administrator performs verification through a signed-in session.

Upload and approve the tool description, input schema and output schema as content. Publish a version with those exact references, its endpoint binding, effect and price terms. Use getToolState to read the current generation before enabling it. Enabling is a human operation; publication alone does not enable execution.

An OpenAPI import can compile selected operations from an approved specification through compileOpenAPI. Review its output, publish the schema files and verify destinations. Compilation does not activate the imported operations or confer credentials.

Agent definitions pin a tool version’s ID, version and integrity. Revoking a pinned tool can stop subsequent actions even when the agent definition was published earlier.

A call can end without Katara knowing whether it took effect: the reply is lost, times out or does not match the output schema. A tool whose effect is a write declares in reconciliation how that is settled, and one without a declaration is neither published nor executed. A read-only tool declares nothing.

idempotency says how the endpoint recognises a repeated request:

  • natural: the request can be repeated without a second effect. Only a repeatable write can say this.
  • key_header: Katara sends each attempt’s ID in the header named by idempotency_header, and the endpoint acts once per ID. REST tools only; the arguments can never set that header.
  • none: neither holds. Only a non-repeatable write can say this.

method says how an unknown result is settled. With status_lookup, which needs key_header, Katara makes one GET to status_lookup.path under the tool’s destination, with {operation_id} replaced by the ID it sent. A 200 whose body the output schema accepts settles the call as done; any other answer, including a 404 while the endpoint is still working, leaves it unknown. With manual, a person records what happened. A tool with quoted pricing uses protocol for both, since the paid tool protocol carries the request’s identity and its status.

on_unknown is reconcile: an unknown result is never retried automatically, and the run waits until it is settled. An answer the endpoint did send, including a refusal the output schema describes, is a known result.

{
"idempotency": "key_header",
"idempotency_header": "Idempotency-Key",
"method": "status_lookup",
"status_lookup": { "path": "/refunds/{operation_id}" },
"on_unknown": "reconcile"
}

Promotion to Live compares the declaration with the Test version’s, so a promoted tool cannot settle differently from the one that was tested.

checkToolHealth / check_tool_health probes a version’s endpoint the way enabling it does and compares the answer with what was published. matches means the same ownership and capabilities; changed means either differs, and the version cannot be enabled again until you publish a new one; unreachable means no answer passed the probe. A revoked version is reported as revoked without a probe. Nothing is recorded, so each check is a fresh reading. It needs permission to publish tools, and the portal’s tool version page has a Check Endpoint button.

Create an agent identity tied to a service account. Issue a grant for that identity, environment, budget, audience, actions and resources. Delegation can narrow a grant; it cannot enlarge its parent’s authority or remaining depth.

Payer consent and a funded budget are separate from key permissions. Budget amounts are integer strings in atomic credits, with scale 9, or in USDC base units with scale 6 for a budget paid from your wallet. Keep them as strings; converting a large amount to a JavaScript number can lose precision. Purpose caps constrain inference, tools, service work, sandboxes and evaluation independently.

The budget response separates total charged from held. The hold covers the unspent funding reserved for an open budget. Each entry in purpose_usage gives charged and reserved atomic-credit strings for that purpose. Operation reservations include pending or unresolved work. Closing a budget requires those operations to be resolved; cancellation does not prove that an external effect was undone.

Judge inference uses both the inference and evaluation caps. For example, a judge charge of 2 credits appears as 2 under inference and 2 under evaluation, while total charged increases by 2. These rows overlap: use the root total rather than adding the purpose rows. A budget with an evaluation cap of zero cannot admit a paid judge request, even when its inference cap has room. The case being judged is a separate evaluation unit under the evaluation cap alone; see what an assessment costs.

A governed tool action is one tool unit at the tool price in force when the action was admitted. The unit is held against the tool cap when the action is claimed, so a budget whose tool cap cannot cover it cannot run the action. It is charged once the tool’s answer is observed, which for a paid tool includes a receipt saying the effect did not apply. Policy checks, an approval the action waits for, status polling and retries add no unit, and repeating the same action finds the same one.

A call that was dispatched without an observed answer keeps the unit held until reconciliation settles it, and the budget cannot close while it is held. For an action from your own runtime, that is the outcome the owner or an admin records: completed charges the unit at the price the action was admitted at, and not_completed releases it. See Record what an unknown action did. An action that was never dispatched, because it was cancelled or its quote ran out first, releases it.

A paid tool’s own price is separate. The publisher’s charge is held at the quoted maximum and settled from its receipt, and the statement shows it on a tool line with no price version, beside the tool line for the units.

The platform shows this breakdown in the selected budget and run review. TypeScript getBudget / listBudgets and Python get_budget / list_budgets return the same fields. They reject malformed money, a different environment, over-cap usage or inconsistent root totals. Keep atomic amounts as decimal strings; use integer arithmetic when calculating with them.

Use listBudgets to discover budgets visible to the current credential. A signed-in payer can use listPayerConsents to inspect their own consent records. That listing includes revoked and expired records; check those fields, usage class and remaining limits before selecting consent. Neither listing creates new spending permission.

Publish rules for allowed action kinds, origins, approval effects and detector failure. Configure review stages with teams, quorums, recent-authentication limits and MFA requirements. Generate a policy review and inspect its cases. Activation names the exact policy reference, review ID and current generation, and requires a human session.

To see what activating a policy would change, simulatePolicy / simulate_policy decides recorded actions again under it and under the active policy: up to 50 you name, or the 25 most recent. Each result shows both decisions, and affected_grants lists the grants whose actions the two decide differently. Both evaluations take the detectors to find nothing, as a review does, and ignore each action’s expiry, so the result is about the policy rather than the content. Nothing is recorded. The portal’s policy page has a Simulate section.

Before publication, load the current environment with getEnvironment in TypeScript or get_environment in Python. Review its mode and include both positive integers expected_environment_epoch and expected_reset_generation. Publication checks these values while holding current environment authority; a reset during review cannot silently move the policy into a newer generation.

const environment = await client.getEnvironment(scope);
const request = {
...reviewedPolicy,
expected_environment_epoch: environment.authority_epoch,
expected_reset_generation: environment.reset_generation,
};
// Review request, then send it with a caller-chosen key.
await client.publishPolicy(scope, request, publicationKey);

Python uses publish_policy(org_id, environment_id, request, publication_key) with the same required fields. Both SDKs snapshot the reviewed request before resolving credentials. After an unknown result, retain that exact request and key; no SDK mutation retries automatically. A reset or later refusal does not justify silently changing the saved pins. Review and activation continue to use the immutable policy reference, review ID and activation generation.

A changed policy or grant is checked at execution time. A previous approval is not a permanent bypass. After a revision conflict, reload the action or policy rather than submitting an approval for an older version.

  1. Open the approval inbox or use a signed-in client to call listApprovals.
  2. Fetch the current action with getApproval. Check its tool version, destination, budget, maximum cost and effect. can_decide describes your current eligibility.
  3. Read its input through previewApprovalInput, passing the current action digest and approval version.
  4. Submit your explicit approve or deny decision with the same digest and version. Keep the idempotency key if the response is lost.

One vote may not complete a multi-stage or multi-person review. An action can expire or become invalid while waiting. The API refuses machine credentials for approval reads and decisions; an approval helper must use the person’s verified session.

Tool publication and policy review are not evidence that every executor is available in a deployment.

The review and decision examples keep input review separate from the person’s explicit vote. A successful vote does not assert that the action has executed. Continue inspecting the approval stages and the originating run.

An agent you run yourself acts through the same authority as a managed one. Tutorial: act from your own runtime sets it up from the start. The agent needs:

  • an external agent identity, and a key for its service account holding locus:actions:execute and locus:tools:read, plus synapse:memory:write to store arguments and synapse:memory:read, under which they are screened;
  • a grant for Locus (katara:locus) with locus:actions:execute on the tool’s ID, under a funded budget;
  • an active policy in the environment: without one every action is refused with 409 conflict;
  • read access for its service account to the files the tool version pins.

The tool is called only in a Live environment under a live budget. In Test the action is recorded but the tool is not called: a simulation budget cannot pay for the effect, and policy denies tool effects under Test Compute with reason test_compute_effect.

  1. Store the arguments as content in the same environment. A person who manages them approves them before any action uses them; approved arguments can be used again.
  2. Optionally ask authorizeAction / authorize_action what admission would decide. The answer is advisory; nothing is recorded, and it is not permission to act.
  3. Submit the action with submitAction / submit_action under an id you choose. Katara builds the action from the published tool version: destination, effect and cost come from the catalog, never from the request. It then checks the grant, the active policy and the budget.

A submission that needs nothing more runs at once through the tool engine and returns the tool’s output with outcome reported_success; the output is not stored. One waiting for a person’s approval answers 202 with state pending: submit the same request again once it is approved and it runs. Repeating an action that already ran finds it claimed and calls nothing, and its output is not returned again. An unknown outcome means the call may have taken effect; do not submit a different action to repeat it. Find out from the destination what happened, then record it.

Tools that need a Katara-held credential or charge a price answer 409 managed_run_required. Only a managed run can use them.

Use listActions and getAction in TypeScript, or list_actions and get_action in Python, with the action’s organization and environment. Sessions and keys need locus:actions:read. Only the original requester can read this history, and for an action from your own runtime that is the person who issued the agent’s grant, or a key that person issued; organization administration alone does not grant access to another person’s actions. A key restricted to particular resources can read only actions for those resources. Workload credentials cannot use these endpoints.

History includes the pinned tool or operation, policy, budget, recorded decision and current state. It omits action inputs, credentials and reviewer authentication details. For example, a refund request may retain a recorded decision of require_approval while denial_source: reviewer explains why it was denied. A policy rejection instead records denial_source: policy.

authority_status: stale means the authority required for the action is no longer current. Unclaimed actions may become invalidated or expired. execution_claimed: true records a claim to execute; it does not establish that a refund or other external effect completed. Check the originating run and its execution outcome before deciding what to do next. Inspection grants no approval, retry or execution permission.

If inspection returns 409, reload because the action changed during the read. After a Test reset, actions from the previous generation are unavailable. A denial notification identifies the exact action and environment so you can inspect the request that produced it.

An action submitted from your own runtime also reports the outcome its call ended with: reported_success, or unknown when nobody can tell whether the effect happened. An unknown outcome carries reconciliation, which is awaiting until someone records what happened and recorded after.

When a call from your own runtime ends unknown, its tool unit stays reserved and its budget cannot close. Find out from the destination whether the effect happened: its logs, a ticket, the record it would have written. Then the owner or an admin records it, once, under Locus → Actions, where actions awaiting an outcome are listed for them whoever’s agent asked.

OutcomeWhat it does to the bill
completedThe action’s one tool unit is charged, at the tool price in force when the action was admitted
not_completedThe unit is released to the budget’s tool cap, and nothing is charged

Recording needs a signed-in person with the locus:actions:reconcile permission, which owners and admins hold and no API key can. A key, a service account or an agent is refused with 403 human_session_required whatever else it may do, and an older sign-in with 403 reauthentication_required. The record names the evidence and what was found, and it cannot be changed or removed. The unit is settled in the same step. Nothing calls the tool again.

Method and pathWhoResult
GET …/environments/{environment_id}/actions/{action_id}/reconciliationAnyone who reads actionsThe outcome, the unit and the record once there is one
POST …/environments/{environment_id}/actions/{action_id}/reconciliationOwner or admin, as a personThe same, with the record
GET /katara/v1/orgs/{org_id}/reconciliationsAnyone who reads runsactions lists the organization’s actions whose outcome was unknown, outstanding first

The body is outcome, reference (3 to 200 characters: where the evidence is) and note (up to 2,000 characters: what was found), with an Idempotency-Key. The same key and body again return the same record and move no money. A different body under that key, or any second answer for the same action, is 409 conflict: the first record stands, and the unit stays as it left it. An action whose call reported success, or one with no recorded outcome, has nothing to record and also answers 409.

const pending = await client.organizationLifecycle.actionReconciliations(org_id);
for (const action of pending) {
if (action.resolution) continue;
// Only after a person has checked the destination:
await client.resolveActionReconciliation(
{ org_id, environment_id: action.environment_id }, action.action_id,
{ outcome: 'not_completed', reference: ticket, note: finding }, requestKey);
}

TypeScript exposes getActionReconciliation, resolveActionReconciliation and organizationLifecycle.actionReconciliations. Python exposes get_action_reconciliation, resolve_action_reconciliation and organization_lifecycle.action_reconciliations. Both refuse an API key for the resolution before sending anything.

A run that ends awaiting reconciliation is recorded differently, and that record moves no money; see closing an organization.

For an enrolled destination, preview the verified account and connect a credential before selecting its exact version for an action. Connection metadata pins the account, permitted agents and destination. A connection does not replace grants, funding or human action approval.

Read the selected connection again with client.toolCredentials.get in TypeScript or client.tool_credentials.get in Python. Review its verified account, permitted agents, mode and reset generation. Select its exact reference; an active directory entry alone does not establish current availability.

Set the tool manifest’s credential_binding to that reference. For an MCP destination, use protocol: 'mcp', the enrolled HTTPS URL, the exact upstream_tool name, and the reviewed environment (sandbox for Test or live for Live). Keep credential bytes out of the manifest, content schemas and recovery records. Katara resolves the private credential at the execution boundary.

const connection = await client.toolCredentials.get(credentialScope, connectionID);
if (connection.availability !== 'ready') throw new Error('Review connection availability');
const request = {
...reviewedPublication,
manifest: {
...reviewedPublication.manifest,
credential_binding: connection.reference,
},
};
// Review the exact account, destination and schema references before publication.
await client.publishToolVersion(scope, request, publicationKey);

Python uses publish_tool_version(org_id, environment_id, request, publication_key) with the same manifest fields. Both SDKs freeze the request before resolving the session or key. Preserve the reviewed request and caller-chosen idempotency key if publication has an unknown outcome; retries are explicit.

The server checks current account authority and the MCP tool’s capabilities. A connection, previous catalog probe or enabled tool does not bypass current agent permissions, funding or approval. Test credentials require an explicitly enrolled Test destination; they cannot fall back to a production account.

After a claimed MCP call loses its response, inspect the action and originating run and reconcile the external effect. An unknown result is not permission to repeat tools/call. Installed-provider acceptance and operator-reviewed Test/Live separation remain deployment requirements.

Before issuing a root grant, read the exact agent identity and budget in the selected environment. Include the identity’s authority_epoch as the required expected_agent_epoch. The server compares it under the authority lock; it will not silently issue for a renamed, disabled or re-enabled identity’s newer epoch. The selected budget and payer consent must also match current environment authority and reset generation. A pre-reset budget cannot fund a new grant after reset.

For example, after reviewing identity, budget, the permissions and expiry:

const request = {
expected_agent_epoch: identity.authority_epoch,
agent_id: identity.id,
budget_id: budget.id,
audience: 'katara:locus' as const,
actions: reviewedActions,
resources: reviewedResources,
expires_at: reviewedExpiry,
remaining_depth: 0,
};
const receipt = await client.issueGrant(scope, request, requestKey);

Python uses issue_grant(org_id, environment_id, request, request_key) with the same JSON fields. An operation receipt identifies the resulting grant; use getGrant / get_grant to inspect it. A grant does not authorize a human approval on the agent’s behalf.

For an interrupted issue request, retain the original account, organization, environment, reviewed body and idempotency key. After signing back into the same account, inspect the current scope and explicitly retry that exact request. Neither SDK automatically retries. Do not replace the agent epoch, budget, permissions or expiry under a pending key. If authority changed, reload and review a new request with a new key. After Test reset, discard recovery from the previous generation; the server refuses issuance against its old funding authority.

Revocation takes the exact grant ID and original request key: revokeGrant(scope, grantID, requestKey) or revoke_grant(org_id, environment_id, grant_id, request_key). Retry an uncertain revocation with those same values. Revocation is final and cannot grant authority or redirect to another grant. It invalidates authority derived through that grant, but does not undo a completed tool effect or promise a refund.

Delegation is a separate operation for eligible authenticated agents, not a human root-grant form. It keeps the existing parent chain, current execution identity and permission restrictions; the root issue review field does not alter its contract. Keep all key secrets and action inputs out of grant recovery metadata.