[AVAILABLE], every command below runs against the repo’s real endpoints,
scripts, and error paths.
Goal
Take a policy from “I have an idea for a rule change” to “it’s approved and live,” using the real maker-checker flow, not a mock of it. Cover the full lifecycle, the one-shot scripts that make it reliable, and every failure mode this flow actually produces.Prerequisites
- Read Policies and the decision and
Write your first policy first — this guide assumes you can
already write a valid
policy.json. - A maker credential (any
callerIdinPARMANA_API_KEYS) to propose with. - A checker credential, genuinely distinct from the maker — see Step 0 below if you don’t have one yet.
Why this exists, in one paragraph
Policy content decides real outcomes: who gets paid, what gets deployed, what an AI agent is allowed to do. A single person silently changing that content, with no second party and no durable record, was an unacceptable gap. Now, a policy change only takes effect after one person proposes it and a genuinely different person reviews and cryptographically approves it — enforced by the API itself (SameActorCannotApproveOwnChangeError), not by convention.
Steps
0. Get a checker credential (one-time, per reviewer)
A checker needs a bearer key and a step-up keypair, generated together:entry JSON block (hash +
public key only — safe to store or share, unlike the other two). An operator appends entry to
the PARMANA_API_KEYS environment variable and redeploys; the checker keeps the bearer key and
the private key file on their own machine, never in this repo.
1. Propose
201 with a pendingPolicyChangeId, or 400 if PolicyValidator finds a fact with no
boundSignals/unboundSignalReasons entry, an oversized matches regex, or a malformed body.
2. Review
proposedContent.rules and the proposal’s reason. The response includes diff.current
vs. diff.proposed, plus coverageWarnings/ruleConflicts if anything’s worth a second look.
This is a judgment call — nothing in this system makes it for you.
3. Sign (on the checker’s own machine, only)
stepUpAuthorization envelope, valid for 120 seconds. This never contacts the API —
signing is entirely local.
4. Submit
.../reject with
{"rejectionReason": "...", "stepUpAuthorization": ...}.
5. The one-shot alternative (recommended)
Chaining sign-then-submit by hand across a 120-second window is fragile — see Troubleshoot below for exactly how.scripts/local-review-action.ts does steps 3 and 4 in one process:
scripts/refresh-approved-policy-content.ts does steps 1 through 4 in one shot, always using the
current on-disk file content rather than a possibly-stale hand-copied snapshot:
Verify
Troubleshoot
{"error":"authentication required"}, 401. The bearer key is missing, wrong, or stale, or the header is missing the literal wordBearer(Authorization: <key>alone doesn’t work). Confirm your env var actually holds a value in this shell before assuming the key itself is bad.STEP_UP_AUTHORIZATION_INVALID, 403, even immediately after signing. The server verifies your signature against whicheverstepUpPublicKeyis currently configured for yourcallerId— not whichever key produced the envelope. This happens most often after rotating a credential: the bearer key gets updated everywhere, but the local.pemfile doesn’t get overwritten with the new private key, so every signature is made with an orphaned key. Confirm the match: derive the public key from your local file (node -e "const c=require('crypto');const f=require('fs');console.log(c.createPublicKey(c.createPrivateKey(f.readFileSync('checker.step-up.private.pem','utf8'))).export({type:'spki',format:'pem'}))") and compare it to thestepUpPublicKeyinPARMANA_API_KEYS.Internal Server Error, 500, with server logs showingInvalid JSONfrombody-parser. The request body wasn’t valid JSON by the time it reached the server — almost always a shell quoting problem (PowerShell mangling a long inline-d '{"a":"b"}'when passing it to a native executable), not a real JSON bug. Write the body to a file and use--data-binary "@body.json"instead of an inline string, or use the one-shot scripts above, which build the request in Node and never pass JSON through a shell.- Approved content is missing fields you expect, e.g.
boundSignals. You approved a proposal that was created a long time before the live file gained those fields. Proposals are frozen snapshots — nothing re-checks them against the current file whilePENDING_APPROVAL. Usescripts/refresh-approved-policy-content.tsto always propose from the live file, then confirm with--full-scan. SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE, 403. The approving bearer key belongs to the samecallerIdthat proposed the change. There is no override — use a genuinely distinct checker credential.
Next
Write your first policy
What a valid policy.json looks like, and how first-match-wins evaluation
works.
Verify a trust record independently
Check a signed artifact’s signature without Parmana running at all — the
same discipline this flow’s step-up signing uses.