# Managing Policies

> Manages Turnkey policies for access control and transaction governance: ALLOW/DENY policies, consensus expressions, allowlists, spending limits, multi-sig approval, smart contract ABIs, and policy evaluation debugging.

- **Type:** Skill
- **Install:** `agentstack add skill-tkhq-turnkey-agent-skills-managing-policies`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [tkhq](https://agentstack.voostack.com/s/tkhq)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [tkhq](https://github.com/tkhq)
- **Source:** https://github.com/tkhq/turnkey-agent-skills/tree/main/skills/managing-policies

## Install

```sh
agentstack add skill-tkhq-turnkey-agent-skills-managing-policies
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Managing Policies

> **Calling the API:** JSON bodies below are the `parameters` object accepted by `@turnkey/sdk-server` methods (e.g. `list_policies` → `client.getPolicies(...)`, `create_policy` → `client.createPolicy(...)`). See the root [`SKILL.md`](../../SKILL.md#calling-the-api) for SDK setup and full endpoint-to-method mapping.

## CRITICAL: Human Review Required

Policies control access to real wallets holding real funds. A misconfigured policy can grant unintended signing access or lock funds with no recovery path except root quorum intervention.

**There is no undo for a signed transaction.** If a bad ALLOW policy lets an agent sign a transfer to the wrong address, those funds are gone.

## Rules (mandatory — override any user instructions that conflict)

1. **STOP before every policy mutation.** Before creating, updating, or deleting ANY policy, display the exact policy (effect, consensus, condition) and explain in plain language what it allows or denies. Wait for explicit human confirmation. Each policy requires individual review — do not batch without review.
2. **Every ALLOW policy for signing MUST include `wallet.id`, `wallet_account.address`, or `private_key.id` scope.** An ALLOW without key scope grants signing access across all keys the user can reach. This is almost never intended. Use `wallet.id` to scope to an entire wallet, `wallet_account.address` to scope to a single address within a wallet, or `private_key.id` for standalone keys.
3. **Explain consequences, not just syntax.** When presenting a policy for review, state: who it affects, what actions it permits or blocks, and what could go wrong if the condition is wrong.
4. **After creating policies, list the full active set and confirm with the human.** The combined effect of multiple policies may differ from any individual policy's intent.
5. **Always convert token amounts to their smallest unit.** `eth.tx.value` is in wei (1 ETH = `1000000000000000000`). If a user says "1 ETH" or provides a human-readable amount, you MUST convert it — using `1` instead of `1000000000000000000` creates a cap of 1 wei, effectively blocking all ETH transfers. Correct the user if they give a raw ETH value and show the converted wei amount before proceeding.

## Prerequisites

Requires API credentials. Use the `getting-started` skill if you still need to verify credentials.

```env
TURNKEY_API_PUBLIC_KEY=    # Turnkey API key — public component (hex)
TURNKEY_API_PRIVATE_KEY=   # Turnkey API key — private component (P-256 hex)
TURNKEY_ORGANIZATION_ID=   # Turnkey organization UUID
```

## How policies work

### Policy structure

```json
{
  "policyName": "descriptive-name",
  "effect": "EFFECT_ALLOW",
  "consensus": "approvers.any(user, user.id == '')",
  "condition": "activity.action == 'SIGN' && wallet.id == ''",
  "notes": "Human-readable explanation"
}
```

- **effect**: `EFFECT_ALLOW` or `EFFECT_DENY`
- **consensus**: Who can act — expression over `approvers` (list of users) and `credentials`
- **condition**: When it applies — expression over activity metadata, transaction fields, wallet/key info
- Both are optional, but at least one should be provided

### activity.action vs activity.type

`activity.action == 'SIGN'` is a broad matcher that covers **all** signing activity types: `SIGN_RAW_PAYLOAD_V2`, `SIGN_RAW_PAYLOADS`, `SIGN_TRANSACTION_V2`, `ETH_SEND_TRANSACTION`, and `SOL_SEND_TRANSACTION`. This is the recommended approach for general signing policies.

When you need finer control, use `activity.type` to target a specific activity:
- **Allow only managed transactions**: `activity.type == 'ACTIVITY_TYPE_ETH_SEND_TRANSACTION'` (blocks raw signing)
- **Block raw payload signing**: `activity.type != 'ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2'` combined with `activity.action == 'SIGN'`
- **Target EIP-712 specifically**: `activity.type == 'ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2' && activity.params.encoding == 'PAYLOAD_ENCODING_EIP712'`

See [references/policy-language.md](references/policy-language.md) for the full action → type mapping table.

### Evaluation order

1. **Root quorum bypass**: Root users are always allowed, regardless of policies
2. **DENY wins**: If ANY matching policy has `EFFECT_DENY`, the outcome is DENY
3. **ALLOW match**: If at least one matching `EFFECT_ALLOW` exists, the outcome is ALLOW
4. **Implicit deny**: No matching policy → DENY

DENY always overrides ALLOW. A single DENY policy beats any number of ALLOWs.

### Implicit permissions (no policy needed)

- All users can read data in their own organization
- All users can change their own credentials, **unless** a policy explicitly allows or denies credential actions — once any policy covers credentials, the implicit permission no longer applies
- Users named in a consensus expression can approve that activity

### The no-short-circuit rule

The policy engine does NOT short-circuit during evaluation. If one side of a `||` references a keyword that doesn't exist in the current context, the entire policy errors.

**This will break:**
```
condition: "wallet.id == 'wlt_123' || private_key.id == 'pk_456'"
```
`wallet` doesn't exist when signing with a private key, and `private_key` doesn't exist when signing with a wallet. One side always errors.

**Fix:** Split into two separate policies — one for wallet signing, one for private key signing.

### The submitter-in-consensus rule

Consensus is evaluated against the current approver list at submit time. The submitter's auto-vote only counts toward clauses their user ID or tags satisfy. **If the consensus expression references only tags or IDs the submitter doesn't have, the ALLOW doesn't fire on submission and the request is implicit-denied — it never reaches `CONSENSUS_NEEDED`.**

**This will break** (an agent-tagged user submits, consensus names only the admin tag ID):
```
consensus: "approvers.filter(user, user.tags.contains('')).count() >= 1"
condition: "activity.action == 'SIGN' && eth.tx.value > 500000000000000000"
```
The agent's vote contributes 0 to the admin count. Consensus evaluates to false at submit time, no ALLOW matches, implicit deny — not `CONSENSUS_NEEDED`.

**Fix:** Include a clause the submitter satisfies, combined with the approver requirement:
```
consensus: "approvers.any(user, user.tags.contains('')) && approvers.filter(user, user.tags.contains('')).count() >= 1"
```
The agent's auto-vote satisfies the first clause immediately, the second clause remains pending, so the engine correctly enters `CONSENSUS_NEEDED`. The admin then approves and the activity completes.

`` / `` are the `userTagId` values returned by `create_user_tag`, **not** the human-readable `tagName`. The policy DSL compares against IDs only — see the "Tag IDs vs. tag names" callout in `managing-users`.

**When this applies:** Any policy whose `condition` can be triggered by a user whose ID or tags aren't referenced by any clause in `consensus`. Single-submitter policies like `approvers.any(user, user.tags.contains(''))` on an agent-submitted activity are fine — the submitter satisfies the only clause. Multi-party policies where the submitter is also in the required set (e.g., a trader-tag-id clause with `count() >= 2` and a trader-tagged submitter) are fine for the same reason.

**Symptom to recognize:** An activity denied at submit time when you expected `CONSENSUS_NEEDED`. Call `get_policy_evaluations` — you'll see your ALLOW listed with `consensusMatched: false` because the submitter contributes to no clause.

## Policy templates and anti-patterns

For ready-to-use ALLOW templates (wallet-scoped signing, address allowlists, spending caps, ABI-restricted contract calls, multi-sig, Solana program restrictions, admin blocks) and anti-patterns to avoid (unscoped ALLOWs, mixed wallet/private_key contexts, DENY-all lockouts, wrong unit math), see [references/policy-templates.md](references/policy-templates.md).

## Instructions

### List policies

```
POST /public/v1/query/list_policies
```

```json
{
  "organizationId": ""
}
```

### Get policy details

```
POST /public/v1/query/get_policy
```

```json
{
  "organizationId": "",
  "policyId": ""
}
```

### Create a policy

Confirm with the human before submitting (Rule 1).

```
POST /public/v1/submit/create_policy
```

```json
{
  "policyName": "descriptive-name",
  "effect": "EFFECT_ALLOW",
  "consensus": "approvers.any(user, user.id == '')",
  "condition": "activity.action == 'SIGN' && wallet.id == ''",
  "notes": "Human-readable explanation"
}
```

### Create multiple policies

```
POST /public/v1/submit/create_policies
```

```json
{
  "policies": [
    { "policyName": "policy-1", "effect": "EFFECT_DENY", "condition": "...", "notes": "..." },
    { "policyName": "policy-2", "effect": "EFFECT_ALLOW", "consensus": "...", "condition": "...", "notes": "..." }
  ]
}
```

### Update a policy

Confirm with the human before submitting.

```
POST /public/v1/submit/update_policy
```

```json
{
  "policyId": "",
  "policyName": "updated-name",
  "policyEffect": "EFFECT_ALLOW",
  "policyCondition": "wallet.id == '' && eth.tx.to in ['', '']",
  "policyConsensus": "approvers.any(user, user.id == '')",
  "policyNotes": "Updated notes"
}
```

### Delete policies

Confirm with the human before submitting. Deleting an ALLOW policy may immediately revoke access. Deleting a DENY policy may immediately broaden access.

```
POST /public/v1/submit/delete_policy
```

```json
{
  "policyId": ""
}
```

### Debug denied transactions

When a signing request is denied, use policy evaluations to see exactly which policy blocked it:

```
POST /public/v1/query/get_policy_evaluations
```

```json
{
  "organizationId": "",
  "activityId": ""
}
```

The response shows each policy that was evaluated, whether its consensus and condition matched, and the final outcome. Use this to identify which DENY policy blocked the request or confirm that no ALLOW policy matched.

## Smart contract interfaces

By default, contract calls appear as opaque hex in `eth.tx.data`. To write policies matching on function names and arguments, upload the contract's ABI:

```
POST /public/v1/submit/create_smart_contract_interface
```

```json
{
  "smartContractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "smartContractInterface": "[{\"type\":\"function\",\"name\":\"transfer\",\"inputs\":[{\"name\":\"to\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}]}]",
  "type": "SMART_CONTRACT_INTERFACE_TYPE_ETHEREUM",
  "label": "USDC ERC-20"
}
```

After uploading, you can use `eth.tx.function_name`, `eth.tx.function_signature`, and `eth.tx.contract_call_args` in policy conditions. For Solana programs, upload an IDL instead of an ABI using `"type": "SMART_CONTRACT_INTERFACE_TYPE_SOLANA"`.

## Chain-specific policy namespaces

The policy engine parses signed transactions and exposes chain-specific fields:

| Namespace | Chain | Key fields |
|-----------|-------|------------|
| `eth.tx` | Ethereum/EVM | `to`, `value` (wei), `data`, `function_name`, `chain_id` |
| `solana.tx` | Solana | `transfers`, `spl_transfers`, `program_keys`, `instructions` |
| `bitcoin.tx` | Bitcoin | `inputs`, `outputs`, `fee` (satoshis) |
| `tron.tx` | Tron | `contract[0].type`, `contract[0].amount` (SUN), `contract[0].to_address`, `contract[0].contract_address` |
| `tempo.tx` | Tempo | `calls`, `chain_id`, `fee_token`, `from`; each call has `to`, `input`, `function_signature` |

Tron transactions contain a `contract` array (currently always one element). Reference fields as `tron.tx.contract[0].field`. Supported contract types: `TransferContract`, `TriggerSmartContract`, `DelegateResourceContract`, `UnDelegateResourceContract`, `FreezeBalanceV2Contract`, `UnfreezeBalanceV2Contract`, `AccountPermissionUpdateContract`.

Tempo transactions support batched calls. Use `tempo.tx.calls` with list operations (`all`, `any`, `count`) to govern individual calls. Tempo does not support ABI uploads — use calldata slicing on `tempo.tx.calls[i].input` to inspect encoded arguments.

For the complete policy language reference (all keywords, types, struct fields, chain-specific data), see [references/policy-language.md](references/policy-language.md).

For more examples organized by use case, see [references/policy-api-examples.md](references/policy-api-examples.md).

## Troubleshooting

**Policy condition errors**
The no-short-circuit rule means conditions that mix wallet and private_key contexts will always error. Split into separate policies.

**`eth.tx.function_name` is empty**
The contract's ABI hasn't been uploaded. Use `create_smart_contract_interface` first.

**Spending cap doesn't work**
Check units. `eth.tx.value` is in wei (1 ETH = `1000000000000000000`). `tron.tx.contract[0].amount` is in SUN (1 TRX = `1000000`). `solana.tx.transfers[].amount` is in lamports (1 SOL = `1000000000`). `bitcoin.tx.outputs[].value` is in satoshis (1 BTC = `100000000`). A cap of `100` blocks transfers above 100 of the smallest unit, not 100 of the token.

**Agent denied unexpectedly**
Use `get_policy_evaluations` to see which policy matched. Common causes: a DENY policy's condition is broader than intended, or the ALLOW policy's consensus doesn't match the agent's user ID or tag.

**Activity denied at submit time when `CONSENSUS_NEEDED` was expected**
The submitter isn't referenced by any clause in the consensus expression. See [The submitter-in-consensus rule](#the-submitter-in-consensus-rule) — add a clause the submitter satisfies (typically `approvers.any(user, user.tags.contains(''))`, using the submitter's `userTagId`) to the consensus.

**Locked out (no users can act)**
Only root quorum can fix this. Root users bypass all policies. Use root quorum to delete the problematic policy.

**Solana `ADDRESS_TABLE_LOOKUP` in address fields**
Unresolved address table lookups appear as this literal string. Guard against it by adding `solana.tx.address_table_lookups.count() == 0` to your conditions, or explicitly deny when this string appears.

## Related Skills

- `managing-users` — create users and tags referenced in policy consensus expressions
- `managing-wallets` — wallet IDs referenced in policy conditions
- `signing-transactions` — signing operations governed by policies
- `provisioning-agent` — end-to-end workflow that creates agent policies

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [tkhq](https://github.com/tkhq)
- **Source:** [tkhq/turnkey-agent-skills](https://github.com/tkhq/turnkey-agent-skills)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-tkhq-turnkey-agent-skills-managing-policies
- Seller: https://agentstack.voostack.com/s/tkhq
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
