Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

The config lives at ~/.config/sa-forge-signer/config.toml unless --config or SA_FORGE_SIGNER_CONFIG says otherwise. sa-forge-signer config init writes the example below there (mode 0600, in a 0700 directory it creates if needed) and refuses to overwrite an existing file. It has no keys yet: add the block key new prints for each key, and [serve] if you run serve.

sa-forge-signer config show prints the config as the signer resolves it: the paths, the RPC, and the instruction table if one is set, each key with its effective class, approval, limits, policy digest and whether its key file exists, defaults filled in, then [serve] with each token’s name and keys. It reads no key material, prints no token hashes, and calls no RPC.

Settings

Only class and pubkey on each key, and a token for serve, have to be set; everything else has a default.

SettingDefaultSet it when
cluster"zink-testnet"never: it is the only cluster
rpc_urlthe cluster’s public RPC (https://rpc1.z.ink)you use another RPC you trust (Security). It must be https://; http:// loads only for a loopback host (127.0.0.1, ::1, localhost), such as a local validator, so signed transactions and any API key in the URL never cross a network in clear
explorer_urlthe cluster’s explorer (https://explorer.z.ink)you prefer another block explorer. Used only for links: the explorer field of a sign report and the console’s links; the signer never calls it
key_dirkeys, beside the configkeys live elsewhere (in Docker, /run/keys)
state_dirstate, beside the configthe audit log lives elsewhere (in Docker, /var/lib/sa-forge-signer)
instruction_tablenoneyou want the instruction table’s findings: { path = "...", sha256 = "...", mode = "report" }, the path relative to the config. The signer refuses to start if the file’s sha256 differs or it is for another cluster. report adds findings to each report and audit entry and never refuses; an enforcing mode comes later
[keys.<name>] classrequired"session" for a SAGE-scoped profile key, "wallet" for the profile authority
pubkeyrequiredthe pubkey key new or key import printed
profilenoneyou want key show and the MCP key_show tool to report the key’s profile, or you configure an instruction_table: its profile finding holds the key to this profile
approval"auto" for a session key, "confirm" for a wallet keya key should never sign ("deny"), or a testnet wallet should sign unattended ("auto", with allow_unattended)
allow_unattendedfalsea throwaway testnet wallet key must be "auto" or on a serve token
rate_limit_per_minute30the agent signs faster than that
daily_lamport_cap50000000 (0.05 ZINK)the key spends more in 24 h (fees and transfers it signs)
halt_after_unknown3the key should halt after more or fewer consecutive unknown outcomes; 0 never halts it
require_policy_approvalfalsea change to the key’s policy should stop it signing until a person runs key approve <name> at a terminal (Policy digest)
max_compute_units1400000 (the runtime’s maximum)the key’s payloads should be held to a lower SetComputeUnitLimit
max_compute_unit_price0 (no priority fee)the key may pay a priority fee: the highest SetComputeUnitPrice, in micro-lamports per compute unit
transfer_max_lamportsnone (only the daily cap)the key’s System transfers, summed over one transaction, should stay under a smaller amount
transfer_tononethe key funds other accounts with ZINK, such as a wallet funding its session key or vault
extra_programsnonethe key must call a program outside the built-in list (How it works); add only programs you trust
[serve] listen"127.0.0.1:8790"serve should listen elsewhere (in Docker, "0.0.0.0:8790")
allowed_hosts["127.0.0.1", "localhost"]clients reach serve under another host name
[[serve.tokens]]none: serve will not startalways, for serve: name, the sha256 token new printed, and the keys it may use

The example, as config init writes it:

# sa-forge-signer config. Default location: ~/.config/sa-forge-signer/config.toml
# Relative paths resolve against this file's directory.

cluster = "zink-testnet"
# rpc_url = "https://rpc1.z.ink"   # default for the cluster; https only (http only on loopback)
# explorer_url = "https://explorer.z.ink"   # default for the cluster; for links only
# key_dir = "keys"                 # 0700; one 0600 file per key
# state_dir = "state"              # audit log
# Report what the instruction table (just apps idl-table) says about each payload; never refuses.
# instruction_table = { path = "zink-testnet.json", sha256 = "SHA256_OF_THE_FILE", mode = "report" }

# A SAGE-scoped session key. Signs without asking. The config holds no key index: the agent passes
# the key's profile index (the next free one when granted, not always 1) as forge-mcp's keyIndex.
# Paste the block `sa-forge-signer key new <name> --class session` prints, or uncomment and fill in.
# [keys.alice-session]
# class = "session"
# pubkey = "PUBKEY_FROM_key_new"
# profile = "PROFILE_ADDRESS"
# rate_limit_per_minute = 30
# daily_lamport_cap = 50000000     # 0.05 ZINK
# halt_after_unknown = 3           # halt the key after this many unknown outcomes in a row; 0 never
# require_policy_approval = false  # sign only while the policy digest is the one `key approve` accepted
# max_compute_units = 1400000      # highest SetComputeUnitLimit
# max_compute_unit_price = 0       # highest SetComputeUnitPrice (micro-lamports); 0: no priority fee
# extra_programs = []               # more programs this key may call at the top level. They are
#                                   # NOT trusted as game programs: token accounts can't be created
#                                   # for accounts they own. Add only programs you trust.

# Path A only: the profile authority and asset wallet held by the agent.
# Defaults to approval = "confirm": each signature is approved on the terminal.
# A wallet key cannot be "auto" or listed on a serve token: it signs only through the CLI
# with a person at the terminal. allow_unattended = true lifts both rules; use it only for a
# throwaway testnet key, since the signer does not decode what a profile grant gives away.
# [keys.alice-wallet]
# class = "wallet"
# pubkey = "WALLET_PUBKEY"
# transfer_to = ["PROFILE_ADDRESS", "SESSION_KEY_PUBKEY"]  # fund the vault and the session key
# transfer_max_lamports = 100000000                         # all transfers in one transaction

# `sa-forge-signer-serve`: HTTP at /v1/check and /v1/sign, MCP at /mcp.
# [serve]
# listen = "127.0.0.1:8790"
# allowed_hosts = ["127.0.0.1", "localhost"]
# [[serve.tokens]]                  # from `sa-forge-signer token new <name> --out <file>`
# name = "agent"
# sha256 = "HASH_PRINTED_BY_token_new"
# keys = ["alice-session"]

Tracing

Tracing is off by default. Set SA_FORGE_SIGNER_LOG to error, warn, info, debug or trace to write spans and events to stderr (stdout still carries only the JSON report), and SA_FORGE_SIGNER_LOG_FORMAT=json for one JSON object per line instead of the compact text format. An unknown value stops the signer with an error rather than being ignored. Each span logs its duration (time.busy, time.idle) when it closes:

SpanLevelWhat it times
sign, checkinfothe whole request, with the key name and payload hash; ends with a done event carrying outcome, failed_check and sent
key_lockinfowaiting for the key’s signing lock
audit_readinforeading the key’s recent audit entries, with how many were read
approvalinfowaiting for a person at the terminal (confirm keys)
confirminfosending and polling until the outcome is known
audit_appendinfowriting one audit entry, with its command
rpcdebugeach RPC call, with its method name
requestinfoeach serve request, with the method, path, status and token name

While tracing is on, serve no longer prints its plain access line (POST /v1/sign -> 200 token=agent), so a JSON log holds only JSON: the request span carries the same method, path, status and token name, plus the duration.

Spans never carry key material, partial signers, tokens or their hashes, the RPC URL (it may hold an API key), or caller-supplied text (summary, intent).