This guide walks through a local end-to-end test of the Stripe Identity step-up example using:
- the manifest:
../examples/stripe-identity-step-up.json - the OPA policy:
../examples/stripe-identity-step-up.rego
The flow uses --dry-run, so you can test proof verification, OPA authorization, and audit output without needing a live backend API behind the wrapped shell commands.
- Node 22+
- this repository checked out locally
- either:
opainstalled locally, or- Docker available for running OPA
node bin/agentcli.js validate examples/stripe-identity-step-up.json --jsonYou should see:
{
"ok": true,
"errors": [],
"warnings": []
}If you have the opa CLI:
opa run --server --addr 127.0.0.1:8181 examples/stripe-identity-step-up.regoIf you prefer Docker:
docker run --rm \
-p 8181:8181 \
-v "$PWD/examples:/policy" \
openpolicyagent/opa:latest \
run --server --addr :8181 /policy/stripe-identity-step-up.regoThe example manifest points at:
http://127.0.0.1:8181/v1/data/agentcli/authz/allow
The checked-in example uses a placeholder jwks_uri. For local testing, generate an RSA key pair and write a temporary manifest that embeds the public key directly.
node --input-type=module <<'EOF'
import { generateKeyPairSync } from 'node:crypto';
import { readFileSync, writeFileSync } from 'node:fs';
const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
writeFileSync('/tmp/agentcli-step-up-private.pem', privateKey.export({ type: 'pkcs8', format: 'pem' }));
const manifest = JSON.parse(readFileSync('examples/stripe-identity-step-up.json', 'utf8'));
delete manifest.authorization_proof_profiles[0].jwks_uri;
manifest.authorization_proof_profiles[0].public_key = publicKey.export({ type: 'spki', format: 'pem' });
writeFileSync('/tmp/stripe-identity-step-up.local.json', JSON.stringify(manifest, null, 2));
EOFValidate the generated manifest:
node bin/agentcli.js validate /tmp/stripe-identity-step-up.local.json --jsonThe manifest's identity_profile still expects a runtime bearer token. Because this guide uses --dry-run, the token can be a placeholder.
export BOT_ACCESS_TOKEN="dummy-bot-token"This token matches both:
- the manifest proof check
- the OPA policy in
examples/stripe-identity-step-up.rego
export ACTOR_STEP_UP_JWT="$(
node --input-type=module <<'EOF'
import { createSign } from 'node:crypto';
import { readFileSync } from 'node:fs';
const privateKey = readFileSync('/tmp/agentcli-step-up-private.pem', 'utf8');
const header = Buffer.from(JSON.stringify({
alg: 'RS256',
typ: 'JWT',
kid: 'local-step-up-test'
})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
iss: 'https://auth.example.com',
aud: 'agentcli',
sub: 'user_demo',
org_id: 'org_demo',
on_behalf_of_user_id: 'user_demo',
delegation_grant_id: 'grant_demo',
run_id: 'run_demo',
agent_id: 'support-bot',
verification_ref: 'vs_local_test',
verification_level: 'document',
verification_verified_at: '2026-04-07T12:00:00Z',
step_up_policy: 'stripe_identity_sensitive_ops',
exp: Math.floor(Date.now() / 1000) + 3600
})).toString('base64url');
const signer = createSign('RSA-SHA256');
signer.update(`${header}.${payload}`);
process.stdout.write(`${header}.${payload}.${signer.sign(privateKey).toString('base64url')}`);
EOF
)"This task does not require step-up proof or OPA authorization.
node bin/agentcli.js exec /tmp/stripe-identity-step-up.local.json list-safe-state --dry-run --signer none --jsonWhat to look for:
"ok": true- no authorization proof requirement
- resolved identity from the
ops-botprofile
node bin/agentcli.js exec /tmp/stripe-identity-step-up.local.json view-sensitive-customer --dry-run --signer none --jsonWhat to look for in the JSON output:
"ok": trueauthorization_proof.verifiedistrueauthorization_proof.signature_verifiedistrueauthorization.decisionispermitactor_context.org_idisorg_demoactor_context.verification.refisvs_local_teststep_up.step_up_policyisstripe_identity_sensitive_ops
Unset the JWT and rerun the sensitive task:
unset ACTOR_STEP_UP_JWT
node bin/agentcli.js exec /tmp/stripe-identity-step-up.local.json view-sensitive-customer --dry-run --signer none --jsonExpected result:
- command fails
- error indicates the authorization proof value was not available
Generate a JWT that still satisfies the manifest proof check but fails the OPA policy by using the wrong org_id.
export ACTOR_STEP_UP_JWT="$(
node --input-type=module <<'EOF'
import { createSign } from 'node:crypto';
import { readFileSync } from 'node:fs';
const privateKey = readFileSync('/tmp/agentcli-step-up-private.pem', 'utf8');
const header = Buffer.from(JSON.stringify({
alg: 'RS256',
typ: 'JWT',
kid: 'local-step-up-test'
})).toString('base64url');
const payload = Buffer.from(JSON.stringify({
iss: 'https://auth.example.com',
aud: 'agentcli',
sub: 'user_demo',
org_id: 'org_wrong',
on_behalf_of_user_id: 'user_demo',
delegation_grant_id: 'grant_demo',
run_id: 'run_demo',
agent_id: 'support-bot',
verification_ref: 'vs_local_test',
verification_level: 'document',
verification_verified_at: '2026-04-07T12:00:00Z',
step_up_policy: 'stripe_identity_sensitive_ops',
exp: Math.floor(Date.now() / 1000) + 3600
})).toString('base64url');
const signer = createSign('RSA-SHA256');
signer.update(`${header}.${payload}`);
process.stdout.write(`${header}.${payload}.${signer.sign(privateKey).toString('base64url')}`);
EOF
)"Now rerun:
node bin/agentcli.js exec /tmp/stripe-identity-step-up.local.json view-sensitive-customer --dry-run --signer none --jsonExpected result:
- proof verification still succeeds
- OPA returns
deny agentclifails withauthorization_denied
Because the example workflow uses contract.audit: "always", both successful dry-runs and authorization failures are written to the audit log.
node bin/agentcli.js audit --limit 5 --jsonWhat to look for in each record:
actor_contextauthorization_proofstep_upauthorizationtrust
For denied runs, look for:
authorization_error
For proof failures, look for:
authorization_proof.verified: false
rm -f /tmp/agentcli-step-up-private.pem
rm -f /tmp/stripe-identity-step-up.local.json
unset BOT_ACCESS_TOKEN
unset ACTOR_STEP_UP_JWTvalidatepasses for the checked-in example.validatepasses for the generated local manifest.list-safe-state --dry-runsucceeds without step-up.view-sensitive-customer --dry-runsucceeds with a valid JWT and OPA allow.view-sensitive-customer --dry-runfails when the JWT is missing.view-sensitive-customer --dry-runfails withauthorization_deniedwhen OPA rejects the actor context.