> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy lifecycle and approvals

> End to end: write a policy, propose it, have a second person approve it, how it takes effect, and how a person signs off on a single critical action.

A policy authorizes nothing until one person proposes it and a different person approves it with a signed step up authorization. Once approved, it is the version in effect for its action, with no deploy. Separately, a policy can say that some actions also need a named person's signed approval each time they run. This page covers both, in the order you do them.

## Who does what

| Role               | What they do                          | What they hold                                                        |
| ------------------ | ------------------------------------- | --------------------------------------------------------------------- |
| Author             | Writes the policy file                | A clone of the repository                                             |
| Proposer (maker)   | Sends the policy for approval         | An API key added as a human (`credentialHolderType USER`)             |
| Approver (checker) | Reviews and approves or rejects it    | Their own API key added as a human, and their own step up private key |
| Operator           | Adds API keys, approver keys, deploys | Access to the deployment                                              |
| Agent              | Sends requests that name the policy   | A service API key                                                     |
| Action approver    | Signs off on one critical action      | Their own approval private key                                        |

The proposer and the approver must be two different people. The server refuses an approval from the proposer's API key, but it cannot tell whether two keys belong to two people: keep each key on its owner's machine.

## Part 1: from a policy file to a policy in effect

```mermaid theme={null}
sequenceDiagram
    participant A as Author
    participant P as Proposer
    participant S as Parmana
    participant R as Approver
    participant G as Agent
    A->>A: Write policies/name/version/policy.json
    P->>S: POST /policies/name/version/pending-changes
    S-->>P: 201, pendingPolicyChangeId, PENDING_APPROVAL
    R->>S: GET /policies/pending-changes?status=PENDING_APPROVAL
    R->>R: Sign step up authorization (own key, 120 s)
    R->>S: POST /policies/pending-changes/id/approve
    S->>S: Check, sign approval record, save policy
    S-->>R: 200, APPROVED
    G->>S: POST /execute naming name at version
    S->>S: Version in effect? Approval record valid? Evaluate
```

### 1. Write the policy

A policy is a JSON file at `policies/<name>/<version>/policy.json`: `policyId`, `policyVersion`, `schemaVersion`, and ordered `rules`, where the first matching rule decides. See [Write your first policy](/guides/write-your-first-policy) for rules and operators. Three optional sections tie signals to reality:

* `boundSignals`: a signal must equal a field of the request, such as `"refundAmount": "parameters.amount"`.
* `unboundSignalReasons`: why a signal the rules read is not bound. Every fact a rule reads must be bound, declared as an approval signal, or given a reason here, or the policy is refused.
* `approvalSignals`: signals that count as `true` only with a person's signed approval (Part 2).

A new version of a live policy is a new folder, such as `customer-refund/1.2.0`, never an edit of an approved one. Check it before proposing: `npx vitest run packages/policy/tests/unit/ReferencePolicies.test.ts` validates every policy file under `policies/`.

### 2. Set up the people, once

* **Proposer and approver API keys**, each added as a human. On a self hosted deployment: [Manage API keys](/self-hosted/api-keys). Anything else gets `403 NON_HUMAN_CALLER_DENIED` on every governance call.
* **The approver's step up key**, made on the approver's own machine. Only the public half goes to the operator:

```bash theme={null}
openssl genpkey -algorithm ed25519 -out step-up.private.pem
openssl pkey -in step-up.private.pem -pubout -out step-up.public.pem
```

The operator registers `step-up.public.pem` on the approver's API key and deploys.

### 3. Propose

The request carries the whole policy and a reason. The name and version in the URL must match `policyId` and `policyVersion` in the content, and the content must pass validation.

<CodeGroup>
  ```bash Bash theme={null}
  export PARMANA_URL=https://your-parmana-host
  export PROPOSER_KEY=<the proposer's API key>

  printf '{"reason":"Why this should take effect.","proposedContent":%s}' \
    "$(cat policies/customer-refund/1.1.0/policy.json)" > proposal.json

  curl -s -X POST $PARMANA_URL/policies/customer-refund/1.1.0/pending-changes \
    -H "Authorization: Bearer $PROPOSER_KEY" -H "Content-Type: application/json" \
    --data @proposal.json
  ```

  ```powershell PowerShell theme={null}
  $url = "https://your-parmana-host"
  $key = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host "Proposer API key" -AsSecureString)))
  $policy = Get-Content policies/customer-refund/1.1.0/policy.json -Raw
  $body = '{"reason":"Why this should take effect.","proposedContent":' + $policy + '}'
  $r = Invoke-RestMethod -Method Post -Uri "$url/policies/customer-refund/1.1.0/pending-changes" `
    -Headers @{ Authorization = "Bearer $key" } -ContentType "application/json" `
    -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
  $r.pendingPolicyChangeId
  ```
</CodeGroup>

The response is `201` with a `pendingPolicyChangeId` and status `PENDING_APPROVAL`. Only one open proposal per policy version is allowed; a second gets `409 CONFLICT`. Send the id to the approver.

### 4. Review

The approver lists what is waiting and reads the proposed content before signing:

```bash theme={null}
curl -s "$PARMANA_URL/policies/pending-changes?status=PENDING_APPROVAL" \
  -H "Authorization: Bearer $APPROVER_KEY"
```

### 5. Approve or reject

The approver signs a step up authorization for this one change and this one action, on their own machine, then sends it within its lifetime (120 seconds at most). The SDKs sign too: `signPolicyChangeStepUp()` in TypeScript, `sign_policy_change_step_up()` in Python.

<CodeGroup>
  ```bash Bash theme={null}
  export CHANGE_ID=<the pendingPolicyChangeId>

  npx tsx scripts/sign-policy-change-step-up.ts \
    --private-key-file step-up.private.pem --key-id <approver caller id> \
    --pending-policy-change-id "$CHANGE_ID" --action approve > signed.txt

  curl -s -X POST $PARMANA_URL/policies/pending-changes/$CHANGE_ID/approve \
    -H "Authorization: Bearer $APPROVER_KEY" -H "Content-Type: application/json" \
    -d "{\"stepUpAuthorization\":$(grep '^{' signed.txt)}"
  ```

  ```powershell PowerShell theme={null}
  $changeId = "<the pendingPolicyChangeId>"
  $stepUp = npx tsx scripts/sign-policy-change-step-up.ts --private-key-file step-up.private.pem `
    --key-id <approver caller id> --pending-policy-change-id $changeId --action approve | Where-Object { $_ -like '{*' }
  $body = '{"stepUpAuthorization":' + $stepUp + '}'
  Invoke-RestMethod -Method Post -Uri "$url/policies/pending-changes/$changeId/approve" `
    -Headers @{ Authorization = "Bearer $key" } -ContentType "application/json" `
    -Body ([System.Text.Encoding]::UTF8.GetBytes($body))
  ```
</CodeGroup>

To reject, sign with `--action reject` and post to `.../reject` with a `rejectionReason`.

On approve, the server does this, in order, and stops at the first failure:

1. The caller is a verified human, or `403 NON_HUMAN_CALLER_DENIED`.
2. The change exists, or `404 PENDING_POLICY_CHANGE_NOT_FOUND`.
3. The caller is not the proposer, or `403 SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE`.
4. The step up authorization is valid for this change and action, signed by the key registered for this caller, not expired, not used before, or `403 STEP_UP_AUTHORIZATION_INVALID`.
5. It writes a signed approval record: who proposed, who approved, when, the content hash before and after, and the hash of the previous record for this policy version, so records form a chain.
6. It saves the policy content and marks the change `APPROVED`.

### 6. It takes effect

For an action bound to a policy (such as `paytm:refund` to `customer-refund`), the version in effect is the one with the most recent approval, so the approval is the release: no deploy. From then on:

* Agents must name that version. A request naming any other version, including an older one that was approved before, is refused before any rule runs, with a message naming the version in effect.
* To roll back, approve the older version again. The newest approval wins.

Binding an action to a different policy name, or adding a new action, still needs a code change and a deploy.

### 7. What is checked on every request

* The version named is the version in effect for the action.
* The loaded policy has an approval record, the record's signature verifies, and its content hash equals the live policy's. Anything else refuses the request, so a policy edited in the database outside this flow cannot authorize anything.
* The gateway checks the approval record again just before release.
* At startup, the server checks every policy against its latest approval record and logs the result.

## Part 2: a person signs off on one action

Some actions need a person each time, such as a refund above 10000. The policy declares which signal needs a signed approval, and where the request says what the approval is for:

```json theme={null}
"approvalSignals": {
  "managerApproved": { "resourceId": "parameters.orderId", "value": "parameters.amount" }
}
```

`resourceId` is `"target"` or a path into `parameters`; `value` is optional and must point at a number. Without `value`, the approval names exactly one resource, which suits a merge approval (`"resourceId": "target"`, for `acme/api#42`).

```mermaid theme={null}
sequenceDiagram
    participant G as Agent
    participant S as Parmana
    participant M as Manager
    G->>S: Refund 75000, managerApproved false
    S-->>G: 403, amounts above 10000 need a signed manager approval
    M->>S: Finds the refusal (Refusal Records)
    M->>M: sign-approval.ts: this order, up to 75000, 15 min, once
    M-->>G: approval.json
    G->>S: New request, managerApproved true, approvalArtifact
    S->>S: Verify before authorizing, use it once
    S->>S: Gateway verifies again before release
    S-->>G: 200, refund executed, signed record
```

### Set up an action approver, once

```bash theme={null}
npx tsx scripts/generate-approver-key.ts \
  --approver-id manager-priya --key-id manager-priya-key-1 --out-dir ~/.parmana
```

The private key stays on the manager's machine. The operator adds `{ approverId, keyId, revoked: false, publicKeyPem }` to `TRUSTED_APPROVAL_ISSUERS` in `packages/api/src/bootstrap/createApprovalIssuerRegistry.ts`, with the contents of the `.public.pem` file as `publicKeyPem`, and deploys. To revoke, set `revoked: true` and deploy.

### Sign an approval

```bash theme={null}
npx tsx scripts/sign-approval.ts \
  --private-key-file ~/.parmana/manager-priya__manager-priya-key-1.private.pem \
  --approver-id manager-priya --key-id manager-priya-key-1 \
  --capability paytm:refund --resource-id ORD-1042 --max-amount 75000 --out approval.json
```

The agent sends a new request with the approval signal set to `true` and the contents of `approval.json` in `signals.approvalArtifact`. Parmana checks the approver is trusted and not revoked, the Ed25519 signature, the expiry, the action, the resource and the amount taken from the request, never from the agent's signals, and that the approval was not used before. It checks again at the gateway just before release. Without a valid approval the request is refused:

```text theme={null}
Execution rejected: Rejected: declared signal(s) do not match independently verified state (managerApproved=true != verified managerApproved=false).
```

To find refusals waiting for a manager, query the Refusal Records for `matchedRuleId = 'reject-manager-approval-required'` (see [Human approval](/concepts/human-approval#review-refused-requests)).

## Where it is recorded

| Table                                   | Holds                                                                               |
| --------------------------------------- | ----------------------------------------------------------------------------------- |
| `pending_policy_changes`                | Every proposal, its status, who proposed and who resolved it                        |
| `policy_change_approval_records`        | Signed, chained approval records; the latest per name decides the version in effect |
| `policies`                              | The approved policy content                                                         |
| `consumed_policy_change_step_up_nonces` | Step up authorizations already used                                                 |
| `consumed_approval_nonces`              | Action approvals already used                                                       |
| `refusal_records`                       | Every refused request, signed                                                       |

## Keep it honest

* Two people, two machines, two keys. A single person holding both API keys makes the record claim a second person who does not exist.
* Never paste an API key into a chat or a ticket. `Read-Host -AsSecureString` in PowerShell keeps it off the screen. Rotate any key that was shown.
* Approver private keys never leave their owner's machine; only public keys are shared.
