Local signing service for AI agents. Keys never leave the daemon, policy gates every signing request, and all access is via a Unix domain socket.
Status
- Key generation for EVM + Solana (no import)
- Policy file with strict schema and CLI helpers
- Daemon with
get_address,sign_evm_tx,sign_eip2612_permit,sign_sol_tx - Socket and key permissions enforced
- Limitation: Solana signing currently operates on raw message bytes and cannot enforce recipient allowlists, value limits, or chain IDs. Tracking full Solana transaction parsing and policy enforcement in issue #2.
Components
sawCLI: keygen and policy managementsaw-daemon: AF_UNIX server that signs on behalf of walletspolicy.yaml: signing rules per wallet~/.saw/keys: raw binary keys on disk
Flowchart
flowchart LR
subgraph Setup
A[saw gen-key] --> B[Keys on disk]
A --> C[policy.yaml stub]
D[saw policy add-wallet] --> C
E[saw policy validate] --> C
end
subgraph Runtime
F[Agent Process] --> G[AF_UNIX socket]
G --> H[saw-daemon]
H --> I[Policy Check]
H --> J[Key Store]
J --> K[Signer]
I --> K
K --> L[Response]
L --> F
end
Installation
Download the latest release:
# Download and extract the release (replace VERSION and ARCH)
curl -LO https://github.com/daydreamsai/agent-wallet/releases/download/vVERSION/saw-VERSION-linux-x86_64.tar.gz
tar xzf saw-VERSION-linux-x86_64.tar.gz
sudo cp saw saw-daemon /usr/local/bin/Or build from source:
cargo build --release
sudo cp target/release/saw target/release/saw-daemon /usr/local/bin/Quick Start
- Install layout
saw install- Generate a wallet
saw gen-key --chain evm --wallet mainSave the printed address and public key, or retrieve them later with saw address --chain evm --wallet main.
- Edit
~/.saw/policy.yamlto add constraints (the default stub has no limits):
nano ~/.saw/policy.yamlSee Policy Schema below for available fields.
- Validate policy
saw policy validate- Start daemon
saw-daemonSystemd Setup (production)
For production, the included systemd unit runs the daemon as a dedicated user with /opt/saw as the root directory and /run/saw/saw.sock as the socket path.
Create the required user and group:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin saw
sudo groupadd --system saw-agent
sudo usermod -aG saw-agent sawInstall layout and set ownership:
sudo saw install --root /opt/saw
sudo chown -R saw:saw /opt/saw
sudo chgrp -R saw-agent /opt/saw/keysInstall and enable the service:
sudo cp systemd/saw.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now sawVerify it's running:
sudo systemctl status sawThe daemon will listen on /run/saw/saw.sock. Add your agent's service user to the saw-agent group so it can connect to the socket:
sudo usermod -aG saw-agent <agent-user>When using the systemd setup, point clients at the production socket:
export SAW_SOCKET=/run/saw/saw.sockNode.js Client
The @daydreamsai/saw npm package provides a typed client:
npm install @daydreamsai/sawimport { createSawClient } from "@daydreamsai/saw";
const saw = createSawClient();
const address = await saw.getAddress();See packages/saw/README.md for full API docs.
CLI Commands
saw install [--root <path>]saw gen-key --chain <evm|sol> --wallet <name> [--root <path>]saw address --chain <evm|sol> --wallet <name> [--root <path>]saw list [--root <path>]saw policy validate [--root <path>]saw policy add-wallet --wallet <name> --chain <evm|sol> [--root <path>]saw-daemon [--socket <path>] [--root <path>]
All commands support --help for usage details.
wallets:
main:
chain: evm
allowed_chains: [1, 8453]
max_tx_value_eth: 0.05
allow_contract_calls: false
allowlist_addresses:
- "0xabc..."
rate_limit_per_minute: 5Unknown fields are rejected.
Request/Response Examples All messages are JSON on a Unix socket. The daemon reads a single request and replies with a single response.
Get address:
{"request_id":"1","action":"get_address","wallet":"main"}Sign EVM tx (EIP-1559):
{
"request_id":"2",
"action":"sign_evm_tx",
"wallet":"main",
"payload": {
"chain_id": 1,
"nonce": 0,
"to": "0x1111111111111111111111111111111111111111",
"value": "0x0",
"gas_limit": 21000,
"max_fee_per_gas": "0x3b9aca00",
"max_priority_fee_per_gas": "0x3b9aca00",
"data": "0x"
}
}Response:
{
"request_id":"2",
"status":"approved",
"result": {
"raw_tx":"0x...",
"tx_hash":"0x..."
}
}Sign EIP-2612 permit (EIP-712 typed data):
{
"request_id":"3",
"action":"sign_eip2612_permit",
"wallet":"main",
"payload": {
"chain_id": 1,
"token": "0x1111111111111111111111111111111111111111",
"name": "USD Coin",
"version": "2",
"spender": "0x2222222222222222222222222222222222222222",
"value": "1000000",
"nonce": "0",
"deadline": "9999999999",
"owner": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}Response:
{
"request_id":"3",
"status":"approved",
"result": {
"signature":"0x..."
}
}Notes:
- If
allowlist_addressesis set, bothtokenandspendermust be in the allowlist. - If
owneris provided, it must match the wallet address.
Sign Solana tx (message bytes): Warning: The daemon signs raw message bytes only (not full Solana transaction structures). Because of this, it cannot enforce policy checks like recipient allowlists, value limits, or chain IDs for Solana yet.
Recommended mitigation (until full parsing):
- Only send pre-validated messages from a trusted component.
- Use a dedicated Solana wallet with low balances and restrictive operational controls.
{
"request_id":"4",
"action":"sign_sol_tx",
"wallet":"treasury",
"payload": {
"message_base64":"aGVsbG8tc29sYW5h"
}
}Response:
{
"request_id":"4",
"status":"approved",
"result": {
"signature":"...",
"signed_tx_base64":"..."
}
}signed_tx_base64 is a minimal encoding: 1 || signature || message (signature count + signature + message bytes).
Permissions
keys/andkeys/<chain>/are set to0700- key files are set to
0600 - socket is set to
0660to allow group read/write so multiple authorized processes can connect. Access is controlled solely by Unix permissions; the daemon does not perform additional authentication or authorization beyond the socket file owner/group/mode. - Operator guidance: restrict the socket’s group to a dedicated minimal-permission group (create a dedicated group,
chgrpthe socket and key dirs, and avoid adding users to broad groups). - Hardening options: use filesystem ACLs (
setfacl), enforcechown/chgrpon startup, or apply MAC controls (SELinux/AppArmor) to limit which processes can access the socket and key paths. - Example workflow (single service access): create group
saw-agent, add only the service user to that group,chgrp -R saw-agent /opt/saw, ensureaudit.logexists and is monitored, then connect to the daemon via the socket and reviewaudit.logfor access visibility. audit.logis created with0640
Audit Logging
Each request appends a single line to audit.log with:
- timestamp
- wallet
- action
- status (approved/denied)
- tx hash (when applicable)
Notes
- Rate limits are in-memory per daemon process.
- Requests larger than 64 KiB are rejected.
- Daemon exits cleanly on
SIGINTorSIGTERM.