> ## 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.

# 2. Your first governed action

> Run Parmana on your machine from a fresh clone: set it up, watch a refund refused until a person signs, release an action to an external system, then call your own local server from the SDK.

This chapter runs everything on your machine, with no database and no cloud account. Every command and every line of
output below was run on a fresh clone of the repository on 2026-10-01.

## Before you begin

* Node.js 22 or later, and Git.
* About ten minutes. `npm ci` and the first build take most of it.

## 1. Set up a fresh clone

<CodeGroup>
  ```bash bash theme={null}
  git clone https://github.com/pavancharak/AgentLabsBuildathon.git
  cd AgentLabsBuildathon
  cp .env.example .env
  npm ci
  npm run build
  npx tsx scripts/generate-keypair.ts --algorithm ed25519 --key-id default
  npm run generate:gateway-keys
  ```

  ```powershell PowerShell theme={null}
  git clone https://github.com/pavancharak/AgentLabsBuildathon.git
  cd AgentLabsBuildathon
  Copy-Item .env.example .env
  npm ci
  npm run build
  npx tsx scripts/generate-keypair.ts --algorithm ed25519 --key-id default
  npm run generate:gateway-keys
  ```
</CodeGroup>

What each step is for:

| Step | Why |
| - | - |
| `.env` from `.env.example` | Only `.env.example` is in the repository. `.env` is ignored by Git and holds your local settings. |
| `npm run build` | Builds the OpenAPI bundle, every package and the TypeScript SDK the examples import. |
| The `default` key | Signs execution authorizations and records. `keys/` is ignored by Git, so a fresh clone has no key until you make one. |
| The `gateway` key | The gateway signs its own attestations with a separate key. |

If a later step fails with a missing file under `keys/`, one of the two key commands was skipped.

## 2. Watch a person decide

Tutorial 119 runs the real `customer-refund` 1.2.0 policy and the components the server runs, in one process.

```bash theme={null}
npx tsx examples/tutorials/119-human-approval-for-one-action/run.ts
```

It ends with:

```text theme={null}
1. A refund of 5000 with no manager: refused, every refund needs one (the agent's own eligibility and fraud claims are not enough)
  REFUSED   reject-manager-approval-required: Refund rejected. Every refund needs a signed manager approval for this order, covering this amount, in signals.approvalArtifact, with managerApproved true, and every other policy condition satisfied.

2. A refund of 75000 with no manager: refused (the server also writes a signed Refusal Record a manager can review)
  REFUSED   reject-manager-approval-required: Refund rejected. ...

3. The agent sends managerApproved: true with no approval: refused
  REFUSED   approval check: managerApproved=true != verified false

The manager reviews the refusal and signs: this order, up to 75000, 15 minutes, once.

4. The agent sends a new request with the signed approval: approved
  APPROVED  approve-refund-with-manager-approval

4b. The gateway checks the approval again just before release
  PASSED    release stage, approval verified, not consumed

5. The same approval sent again on another request: refused, an approval is used once
  REFUSED   approval check: managerApproved=true != verified false

6. An approval up to 75000 used for 90000: refused, the amount comes from the request
  REFUSED   approval check: managerApproved=true != verified false

7. An approval for another order: refused
  REFUSED   approval check: managerApproved=true != verified false

8. A refund of 150000, even with an approval: refused by the policy maximum
  REFUSED   reject-above-maximum: Refund rejected because the requested refund amount exceeds the maximum of 100000, even with a manager approval.

✓ All 9 steps behaved as the policy and the approval rules require.
```

Read it as the four rules of [Chapter 1](/build-book/01-how-parmana-works) in action:

* The agent's own claims (eligible, fraud check passed) never approve a refund (steps 1 and 2).
* Saying `managerApproved: true` is worth nothing without a signed approval (step 3).
* A signed approval covers one action, one resource, up to one amount, once (steps 5 to 7).
* The policy still applies to an approved request (step 8).

## 3. Release an action to a system Parmana has no code for

Tutorial 123 registers an external connector through maker checker, runs an example endpoint from the SDK on your
machine, and releases an approved request to it.

```bash theme={null}
npx tsx examples/tutorials/123-external-connector/run.ts
```

The parts that matter (the server's own log lines are left out):

```text theme={null}
Step 1: register erp:create-invoice, maker then checker
Approved by checker  : 200 APPROVED
Registration         : active -> https://erp.example.com/parmana/release

Step 3: an approved request is released to the ERP
Release audience     : https://erp.example.com/parmana/release
Approved by          : manager-x
Signature            : ed25519, key default
Result success       : true
Invoices created     : INV-1

Step 4: Parmana sends the same release again
ERP answered         : 200, INV-1
Invoices created     : INV-1

Step 5: a release made for another endpoint
Another endpoint     : 401 release.audience "https://erp.example.com/parmana/release" is not this endpoint (https://payroll.example.com/parmana/release)

Step 6: revoke the registration
Revoke approved      : 200 APPROVED
Next request         : CONNECTOR_NOT_REGISTERED
Invoices created     : INV-1

✓ Registered by two people, released signed, acted on once, refused elsewhere, stopped by a revoke.
```

[Chapter 6](/build-book/06-connect-your-systems) explains each step.

## 4. Run the server and call it from the SDK

The tutorials run Parmana in one process. Now run the server and talk to it over HTTP, as an agent does.

### Make a caller key for your agent

```bash theme={null}
npx tsx scripts/generate-api-key.ts --caller-id local-agent --allowed-capabilities test:fixture-execute
```

It prints the raw key once (`Key       : ...`) and the entry to configure (`{"callerId":"local-agent","keyHash":...}`).
Only the hash is ever stored. Keep the raw key for the agent; put the entry in `PARMANA_API_KEYS`.

### Make an approver key and sign an approval

You play the approver here. On a real deployment the approver is a different person
([Chapter 5](/build-book/05-human-approvals)).

```bash theme={null}
npx tsx scripts/generate-approver-key.ts --approver-id local-test-approver --key-id local-test-approver-key-1 --out-dir ./local-approver
npx tsx scripts/sign-approval.ts --private-key-file ./local-approver/local-test-approver__local-test-approver-key-1.private.pem --approver-id local-test-approver --key-id local-test-approver-key-1 --capability test:fixture-execute --resource-id vendor/vendor-123 --max-amount 100 --out ./local-approver/approval.json
```

The approval expires after 15 minutes unless you pass `--ttl-seconds`, and it can be used once.

### Start the server

`NODE_ENV=test` with `PARMANA_STORAGE=memory` runs with no database. It also registers `test:fixture-execute`, a
connector that needs no credential, and trusts the approver key in `PARMANA_TEST_APPROVER_PUBLIC_KEY_FILE`. The server
refuses to start if that variable is set with any other `NODE_ENV`.

<CodeGroup>
  ```bash bash theme={null}
  NODE_ENV=test \
  PARMANA_STORAGE=memory \
  PARMANA_API_KEYS='[{"callerId":"local-agent","keyHash":"<the hash printed above>","allowedCapabilities":["test:fixture-execute"]}]' \
  PARMANA_TEST_APPROVER_PUBLIC_KEY_FILE=./local-approver/local-test-approver__local-test-approver-key-1.public.pem \
  npm run dev
  ```

  ```powershell PowerShell theme={null}
  $env:NODE_ENV = "test"
  $env:PARMANA_STORAGE = "memory"
  $env:PARMANA_API_KEYS = '[{"callerId":"local-agent","keyHash":"<the hash printed above>","allowedCapabilities":["test:fixture-execute"]}]'
  $env:PARMANA_TEST_APPROVER_PUBLIC_KEY_FILE = ".\local-approver\local-test-approver__local-test-approver-key-1.public.pem"
  npm run dev
  ```
</CodeGroup>

Paste the whole entry the key script printed, not a retyped one. In another terminal:

```bash theme={null}
curl http://localhost:3000/health
# {"status":"UP"}

curl http://localhost:3000/version
# {"error":"authentication required"}

curl http://localhost:3000/version -H "Authorization: Bearer <the raw key>"
# {"name":"Parmana","version":"0.4.0","api":"v1"}
```

Every route except `/health`, `/ready` and the API description routes needs a key.

### Send a request, refused, then approved

Save as `first-action.ts` in `typescript/examples/` (so `@parmana/sdk` resolves to the SDK you built) and run it with
`PARMANA_API_KEY` set to the raw key:

```typescript theme={null}
import { readFileSync } from "node:fs";

import {
  createBusinessTransaction,
  ExecutionRejectedError,
  ParmanaClient,
} from "@parmana/sdk";

const client = new ParmanaClient({
  endpoint: "http://localhost:3000",
  apiKey: process.env.PARMANA_API_KEY!,
});

const me = await client.caller();
console.log(me);

const request = {
  principalId: me.callerId,
  purpose: "My first governed action",
  action: "test:fixture-execute",
  target: "vendor/vendor-123",
  parameters: { amount: 100, currency: "USD" },
  policy: { name: "vendor-payment", version: "2.1.0", schemaVersion: "1.0.0" },
};

const signals = {
  vendorVerified: true,
  invoiceVerified: true,
  paymentApproved: true,
  sufficientFunds: true,
  paymentAmount: 100,
  riskScore: 5,
  vendorId: "vendor/vendor-123", // bound: must equal target
};

try {
  await client.execute(
    createBusinessTransaction({
      ...request,
      signals: { ...signals, humanApproved: false },
    }),
  );
} catch (error) {
  if (!(error instanceof ExecutionRejectedError)) throw error;
  console.log("Refused:", error.message);
}

const approval = JSON.parse(
  readFileSync("../../local-approver/approval.json", "utf8"),
);

const record = await client.execute(
  createBusinessTransaction({
    ...request,
    signals: { ...signals, humanApproved: true, approvalArtifact: approval },
  }),
);

console.log(
  "Approved:",
  record.executions[0].decision.outcome,
  record.trustRecordHash,
);
```

```bash theme={null}
cd typescript/examples
npx tsx first-action.ts
```

Output (your hash differs):

```text theme={null}
{ callerId: 'local-agent', allowedPrincipalIds: [ 'local-agent' ], allowedCapabilities: [ 'test:fixture-execute' ], unrestrictedCapabilities: false }
Refused: Execution rejected: Vendor payment rejected because one or more required policy conditions were not satisfied.
Approved: APPROVED c1ee106cb303c57723128efd46e4dba53fc6ec37f238aa410d9cfe54548120b5
```

`allowedPrincipalIds` defaults to the caller id when the key entry names none. That is why the request acts as
`me.callerId`.

## When something goes wrong

| You see | Cause and fix |
| - | - |
| `401 {"error":"authentication required"}` | No key, a wrong key, or a hash in `PARMANA_API_KEYS` that is not the hash of that key. |
| The server refuses to start, naming `DATABASE_URL` | `NODE_ENV` is not `test`. Outside `test`, storage for replay protection and audit needs Postgres. |
| The server refuses to start, naming the approver file | `PARMANA_TEST_APPROVER_PUBLIC_KEY_FILE` is set while `NODE_ENV` is not `test`. |
| `Refused: ... approval ...` on the second request | The approval expired (15 minutes), was used already, or names another resource or a lower amount. Sign a new one. |
| `Cannot find module '@parmana/sdk'` | Run the script from `typescript/examples/`, after `npm run build`. |

## Next

[Chapter 3](/build-book/03-connect-an-agent) does the same from a real agent, against a real server, in both SDKs.
