AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified Apache-2.0 Self-run

Managing Policies

skill-tkhq-turnkey-agent-skills-managing-policies · by tkhq

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.

No reviews yet
0 installs
24 views
0.0% view→install

Install

$ agentstack add skill-tkhq-turnkey-agent-skills-managing-policies

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-tkhq-turnkey-agent-skills-managing-policies)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Managing Policies? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Managing Policies

> Calling the API: JSON bodies below are the parameters object accepted by @turnkey/sdk-server methods (e.g. list_policiesclient.getPolicies(...), create_policyclient.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.

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

{
  "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 createusertag, **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
{
  "organizationId": ""
}

Get policy details

POST /public/v1/query/get_policy
{
  "organizationId": "",
  "policyId": ""
}

Create a policy

Confirm with the human before submitting (Rule 1).

POST /public/v1/submit/create_policy
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.