paw is an Internet Computer canister that enforces policy-governed request execution for agent-driven EVM actions, including buyer-side x402 payment flows. Agent signing/spending requests need to be approved by a human per default, but authorization can be auto-approved using fine-grained policies. Signing is performed using the Internet Computer's threshold signatures, hence, key material is never exposed to the agent or a cloud provider.
- Deterministic request lifecycle: submit, approve/reject, sign, callbacks, terminalization.
- Multi-tenant user onboarding (
register_user) with optional controller-issued signup invite codes. - Self-service wallet creation (
create_wallet) and wallet-scoped request ownership. - Buyer-side x402 v2 (
exact) flow with fail-closed policy checks. - Canister-owned signing via management canister ECDSA APIs.
- Governed policy mutation with approval workflow and immutable policy versions.
- Stable-memory persistence for users, wallets, requests, audits, replay indexes, and policy history.
src/lib.rs: canister entrypoints and orchestration logic.src/domain/*: domain models and pure validation/transition logic.src/state/*: stable-memory storage and ID generation.tests/*: PocketIC integration tests for lifecycle, policy, and multitenant wallet isolation flows.frontend/src/*: wallet-aware React UI with invite-gated signup and wallet-scoped request views.docs/design/*: shaping/spec/implementation plans and review tasklists.docs/runbooks/agent-invite-signing-runbook.md: operator runbook for invite onboarding and transaction-signing request flow.
This project uses icp-cli as the default canister toolchain.
- Production/autonomous agents must execute a preinstalled, pinned
pawbinary.- Install:
npm install -g @paw/sdk@<exact-version> - Run:
paw ...
- Install:
npxmay be used only for dev/bootstrap and must be version-pinned.npx -y @paw/sdk@<exact-version> paw ...
- Do not allow floating
npx @paw/sdkwithout@<exact-version>in CI, scripts, or unattended agents. - CI/CI-like runners should use an explicit install step (or repository lockfile with deterministic resolution) and invoke
pawby name fromPATH.
Example CI invocation pattern:
npm ci
npx -y @paw/sdk@$(node -p "require('./paw-sdk/package.json').version") paw --version- Rust toolchain with
wasm32-unknown-unknowntarget. icp-cliinstalled and available onPATH.- Node.js
20.19.0+(Vite 7 and SDK tests rely on Node 20 features).
Use nvm use in the repository root to select the pinned Node.js version from .nvmrc.
# Build Rust artifacts
cargo build
# Run unit + integration tests
cargo test --all-targets --all-features
# Run formatting and lint checks
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
# Start local ICP network
icp network start --background
# Deploy frontend + backend (handles backend init/upgrade args)
./scripts/deploy.sh
# Stop local network
icp network stopGit hooks run from .githooks:
pre-commit->./scripts/validate.sh fast --stagedpre-push->./scripts/validate.sh full
Validation modes:
# quick local gate (format/lint + changed-scope tests)
./scripts/validate.sh fast --staged
# full local gate (candid, rust tests, sdk tests, frontend test+build)
./scripts/validate.sh full
# dependency/security audits
./scripts/validate.sh security
# release gate (full + security + coverage + mutation tests)
./scripts/validate.sh releasePocketIC stability note:
scripts/validate.shstarts one shared PocketIC server per run and reuses it across integration targets.- To force tests to use an already-running server, set
PAW_POCKET_IC_SERVER_URL(orPOCKET_IC_SERVER_URL) tohttp://127.0.0.1:<port>.
Optional toggles:
PAW_PRE_PUSH_MODE=releaseto run the release gate during pre-push.PAW_PRE_PUSH_RUN_IGNORED_E2E=1to include ignored real x402 e2e in pre-push.PAW_STRICT_OPTIONAL_TOOLS=1to fail if optional local tools are missing.
scripts/validate.sh auto-detects and uses these tools when installed:
cargo-nextestfor faster parallel test execution.sccachefor Rust compile caching (RUSTC_WRAPPER=sccache).cargo-llvm-covfor coverage output (target/llvm-cov/lcov.info).cargo-mutantsfor mutation testing on critical policy/x402 modules.
Recommended install commands:
cargo install cargo-nextest
cargo install sccache --locked
cargo install cargo-llvm-cov
cargo install cargo-mutants
cargo install cargo-audit
cargo install cargo-denyFor continuous local loops, use either:
cargo watch -x 'fmt --all -- --check' -x 'clippy --all-targets --all-features -- -D warnings' -x 'test --lib --all-features'
bacon clippyThe backend canister has init(args: InitArgs).
icp deploy (all canisters) does not expose per-canister args on the command line in icp-cli 0.1, so use:
./scripts/deploy.shThe script behavior is:
- creates canisters if missing
- deploys frontend
- installs backend with
InitArgson first install - upgrades backend with profile-specific
post_upgradeargs - applies explicit deploy profiles:
local: management canister signing, keydfx_test_key, signup-gate disabledplayground: chain-key testing canister signing (vrqyr-saaaa-aaaan-qzn4q-cai), keyinsecure_test_key_1, signup-gate disabledproduction: management canister signing, keykey_1, signup-gate enabled
Recommended reproducible setup is to store backend init args in a checked-in Candid file:
(
record {
config = opt record {
require_signup_invite_code = opt false;
ecdsa_key_name = opt "dfx_test_key";
signing_backend = opt variant { ManagementCanister };
};
}
)
Then deploy with:
./scripts/deploy.sh --init-args-file ./deploy/backend-init.args.did.exampleDeploy directly to public playground-like environments with:
./scripts/deploy.sh --environment playgroundNote: --environment playground uses dfx --playground under the hood for borrowed canisters
(no pre-minted cycles required for create). Non-playground environments continue to use icp-cli.
The script writes a temporary dfx.json in the project root during this path and restores/removes it on exit.
For playground frontend builds, it injects VITE_BACKEND_CANISTER_ID from the reserved backend canister.
Deploy with explicit production profile:
./scripts/deploy.sh --environment ic --profile productionThe Candid interface is exported from Rust (ic_cdk::export_candid!()) and extracted from the compiled Wasm.
Use:
./scripts/generate-candid.sh backend paw.didDo not hand-edit paw.did.