From d93442fdc13a37324bf05a30eab837570f90da9a Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Aug 2026 18:17:44 -0400 Subject: [PATCH 1/2] docs: wallet_account derivation-path fields for export policies Document six new wallet_account policy-language fields (path, path_indexes, path_hardened, wallet_id, curve, address_format) added for EXPORT_WALLET_ACCOUNT activities, plus a hardened derivation-subtree export policy example. --- features/policies/examples/access-control.mdx | 17 +++++++++++++++++ features/policies/language.mdx | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/features/policies/examples/access-control.mdx b/features/policies/examples/access-control.mdx index 77bbd0a9..3feafdec 100644 --- a/features/policies/examples/access-control.mdx +++ b/features/policies/examples/access-control.mdx @@ -179,3 +179,20 @@ This policy can be used to say, only passkeys are allowed to sign transactions a "condition": "activity.action == 'EXPORT' && wallet_account.address == ''" } ``` + +#### Allow exporting only wallet accounts under a hardened derivation subtree + +This policy allows exporting only wallet accounts whose derivation path is a fully-hardened, +4-segment path of the form `m/8797555'/x'/3'/y'` (a Spark-purpose subtree). Because single-quoted +policy literals cannot contain `'` (apostrophe), the path is matched structurally via +`wallet_account.path_indexes` and `wallet_account.path_hardened` rather than as a string. Note that +policies match the literal stored path; on ed25519, soft and hardened spellings derive the same key. + +```json JSON +{ + "policyName": "Allow exporting only accounts under m/8797555'/x'/3'/y'", + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "activity.kind == 'EXPORT_WALLET_ACCOUNT' && wallet_account.path_indexes.count() == 4 && wallet_account.path_indexes[0] == 8797555 && wallet_account.path_indexes[2] == 3 && wallet_account.path_hardened.all(h, h == true)" +} +``` diff --git a/features/policies/language.mdx b/features/policies/language.mdx index 8d7c1617..4ada8667 100644 --- a/features/policies/language.mdx +++ b/features/policies/language.mdx @@ -116,6 +116,12 @@ The language is strongly typed which makes policies easy to author and maintain. | | exported | bool | Boolean indicating whether or not this wallet has been exported | | | label | string | The label of this wallet | | **Wallet Account** | address | string | The wallet account address | +| | path | string | The account's BIP32 derivation path as stored, e.g. `m/44'/60'/0'/0/0` | +| | path_indexes | list\ | The derivation path child indexes with the hardened bit stripped, e.g. `[44, 60, 0, 0, 0]` | +| | path_hardened | list\ | Positionally paired with `path_indexes`; true where the corresponding path segment is hardened | +| | wallet_id | string | The identifier of the wallet this account belongs to | +| | curve | string | The curve enum name, e.g. `CURVE_SECP256K1` | +| | address_format | string | The address format enum name, e.g. `ADDRESS_FORMAT_COMPRESSED` | | **PrivateKey** | id | string | The identifier of the private key | | | tags | list\ | The collection of tags for the private key | | | imported | bool | Boolean indicating whether or not this private key has been imported | @@ -179,6 +185,15 @@ The language is strongly typed which makes policies easy to author and maintain. Primitives section.{" "} + + {" "} + Single-quoted policy string literals cannot contain `'` (apostrophe), so derivation paths like + `m/44'/60'/0'/0/0` cannot be matched as strings. This is why the Wallet Account derivation path + is exposed structurally via `path_indexes` and `path_hardened` in addition to `path`. The + `path`, `path_indexes`, `path_hardened`, `wallet_id`, `curve`, and `address_format` fields are + currently populated only for `EXPORT_WALLET_ACCOUNT` activities; other activities will follow.{" "} + + #### Nested structs | Struct | Field | Type | Description | From 51271ad7dc61cd55bee092c791e50415af5920bb Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 11 Aug 2026 22:12:40 -0400 Subject: [PATCH 2/2] docs: error semantics and skew guidance for wallet_account path fields Adversarial-review follow-ups: - Clarify that referencing the new wallet_account derivation-path fields on activities other than EXPORT_WALLET_ACCOUNT produces a MissingField evaluation error (policy outcome is an error), not empty values. - Add version-skew rollout guidance: an erroring EFFECT_DENY is skipped while a broad EFFECT_ALLOW still matches (fails open during the skew window) - recommend narrow-allow patterns over broad-allow + path-deny. --- features/policies/examples/access-control.mdx | 7 +++++++ features/policies/language.mdx | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/features/policies/examples/access-control.mdx b/features/policies/examples/access-control.mdx index 3feafdec..dec26672 100644 --- a/features/policies/examples/access-control.mdx +++ b/features/policies/examples/access-control.mdx @@ -196,3 +196,10 @@ policies match the literal stored path; on ed25519, soft and hardened spellings "condition": "activity.kind == 'EXPORT_WALLET_ACCOUNT' && wallet_account.path_indexes.count() == 4 && wallet_account.path_indexes[0] == 8797555 && wallet_account.path_indexes[2] == 3 && wallet_account.path_hardened.all(h, h == true)" } ``` + +The derivation-path fields are populated only for `EXPORT_WALLET_ACCOUNT` activities: on any other +activity, a condition referencing them produces an evaluation error (the policy's outcome is an +error, not a match against empty values). Prefer narrow allow policies like this one over combining +a broad allow with a path-based `EFFECT_DENY`: during rollout windows where some Turnkey components +do not yet know these fields, an erroring deny is skipped while a broad allow still matches (failing +open), whereas a narrow allow that errors fails closed. diff --git a/features/policies/language.mdx b/features/policies/language.mdx index 4ada8667..aaa4dc0a 100644 --- a/features/policies/language.mdx +++ b/features/policies/language.mdx @@ -191,7 +191,22 @@ The language is strongly typed which makes policies easy to author and maintain. `m/44'/60'/0'/0/0` cannot be matched as strings. This is why the Wallet Account derivation path is exposed structurally via `path_indexes` and `path_hardened` in addition to `path`. The `path`, `path_indexes`, `path_hardened`, `wallet_id`, `curve`, and `address_format` fields are - currently populated only for `EXPORT_WALLET_ACCOUNT` activities; other activities will follow.{" "} + currently populated only for `EXPORT_WALLET_ACCOUNT` activities; other activities will follow. + On any other activity, a condition referencing these fields produces a `MissingField` evaluation + error — the policy's outcome is an error rather than a match against empty values (see + [Policy evaluation](#policy-evaluation) in the appendix), so an `EFFECT_ALLOW` policy that + references them does not grant access outside of exports.{" "} + + + + {" "} + During a rollout window in which some Turnkey components have not yet been updated with these + fields (version skew), a condition referencing them can produce an evaluation error even on + `EXPORT_WALLET_ACCOUNT` activities. An erroring `EFFECT_ALLOW` policy fails closed (the export + is denied), but an erroring `EFFECT_DENY` policy is skipped — so pairing a broad allow (e.g. + "allow all exports") with a path-based deny can fail open during the skew window. Prefer narrow + allow policies that match only the derivation paths you intend to permit over broad-allow plus + path-deny combinations.{" "} #### Nested structs