Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/policy-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Issue #226: run the example policy engines, including the shared pre-sign
# policy conformance suite, against the SDK built from this PR. No secrets; no
# network beyond npm.
name: Policy Conformance
on:
pull_request:
paths:
- packages/sdk/**
- examples/x402-policy-daily-budget/**
- .github/workflows/policy-conformance.yml
push:
branches:
- main
paths:
- packages/sdk/**
- examples/x402-policy-daily-budget/**
- .github/workflows/policy-conformance.yml
permissions:
contents: read
jobs:
daily-budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
# --legacy-peer-deps: same reason as sdk-test.yml.
- run: npm install --no-audit --no-fund --legacy-peer-deps
working-directory: packages/sdk
- run: npm run build
working-directory: packages/sdk
# The example links the SDK with file:../../packages/sdk, so this is the build above.
- run: npm ci --no-audit --no-fund
working-directory: examples/x402-policy-daily-budget
- run: npm test
working-directory: examples/x402-policy-daily-budget
2 changes: 1 addition & 1 deletion examples/x402-policy-daily-budget/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ npm run typecheck
npm start # offline demo with fake contexts
```

The SDK is referenced as `file:../../packages/sdk` and only its types are imported, so build it once (`cd ../../packages/sdk && npm install --legacy-peer-deps && npm run build`, as the SDK CI does; `@stellar/mpp` 0.7.1 still declares a peer of `@stellar/stellar-sdk` ^15) before `typecheck`.
The SDK is referenced as `file:../../packages/sdk`, so build it once (`cd ../../packages/sdk && npm install --legacy-peer-deps && npm run build`, as the SDK CI does; `@stellar/mpp` 0.7.1 still declares a peer of `@stellar/stellar-sdk` ^15) before `test` and `typecheck`. `test/conformance.test.ts` runs the SDK's shared policy conformance suite (#226) against this engine through the SDK's real pre-sign gate; CI runs it on every PR that touches the SDK or this example.

## Multi-process: map it to SQL

Expand Down
13 changes: 13 additions & 0 deletions examples/x402-policy-daily-budget/test/conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// The shared pre-sign policy conformance suite (#226) against DailyBudgetPolicy,
// through the SDK's real gate. Needs the SDK built (see README).
import { runPolicyConformance } from 'nirium/dist/x402-policy-conformance.js';
import { DailyBudgetPolicy } from '../src/daily-budget-policy.ts';

runPolicyConformance({
name: 'daily-budget',
// A fresh policy per case, so one case's spend never affects another.
createHook: () => new DailyBudgetPolicy({ dailyCap: 10_000_000n }).hook(), // 1 USDC a day
allow: { amount: '2500000' }, // 0.25 USDC
deny: { amount: '10000001' }, // just over the cap
// No `wait`: this engine never answers WAIT, so the suite turns its ALLOW into one.
});
21 changes: 21 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,27 @@ Only an `ALLOW` that echoes this authorization's `contextHash`, and is still val

**What this does not do.** It is a check on the agent side, not account-level enforcement: code in the same process that holds the raw signer can still call it directly (the L02 limit in #96; so can code that replaces its `signAuthEntry`, which is looked up at call time), and nothing on-chain enforces the policy. It does not solve pending-capacity reservation (L01 in #96: concurrent requests that observe the same remaining capacity): two calls evaluated at the same time can each fit a limit that together they exceed, and an aggregate cap has to be held by your policy. Discussed in [#96](https://github.com/nirium-protocol/nirium/issues/96), where @CodeDeityX laid out the agent-side cases this hook is built against.

**Conformance test for your engine.** The SDK ships one shared test any engine can run ([#226](https://github.com/nirium-protocol/nirium/issues/226)). It puts your hook through the real gate and the real `@stellar/stellar-sdk` signing path, with no RPC and no network, and checks seven cases. An `ALLOW` that echoes the exact `contextHash` must sign exactly once. `DENY`, `WAIT`, an `ALLOW` with a wrong `contextHash`, an `ALLOW` past its `expiresAt`, a malformed answer and an answer after the timeout must each sign nothing. You give it one payment your engine allows and one it denies, plus one it answers `WAIT` to if it ever does. The last four cases start from your engine's own `ALLOW` and change it the way each case names. It is experimental like the hook and is not exported from the package index, so import it from `dist`:

```typescript
// test/conformance.test.ts, run with `node --test` (via tsx for TypeScript)
import { runPolicyConformance } from 'nirium/dist/x402-policy-conformance.js';

runPolicyConformance({
name: 'my engine',
createHook: () => myEngine.hook(), // called once per case: return a fresh one
allow: { amount: '2500000' }, // base units; payTo, asset, url, method are optional
deny: { amount: '50000000' },
// wait: { amount: '1', payTo: 'G...' }, // only if your engine answers WAIT
});
```

**What passing means.** Your engine answers correctly (allows what it should, denies what it should, binds its `ALLOW` to the exact `contextHash`) and the gate fails closed around it. It does **not** validate your policy logic: it only checks the two or three payments you give it, and several cases (wrong hash, expired, malformed, timeout) mostly exercise the gate on top of your engine's `ALLOW`. Test the rules themselves with your own tests.

Your hook is used as-is, so a class instance works: its prototype methods (`currentVersion`, `onDecision`) are called on the instance. The timeout case runs the gate with `timeoutMs` 100 (change it with `runPolicyConformance(target, { timeoutMs })`); your engine still gets its own `timeoutMs` (default 5000 ms) to answer, so a slow remote engine does not fail that case.

Each case is a `node:test` test. To use another runner, `policyConformanceCases(target)` returns the cases as `{ name, run }`. [`examples/x402-policy-daily-budget`](../../examples/x402-policy-daily-budget) runs it in CI.

### MPP Charge (experimental)

```typescript
Expand Down
Loading
Loading