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

# Connect an External Agent

> Complete guide for a new developer: what's needed, why, how to build and send the request, and how to troubleshoot every failure mode — grounded in the actual source, using the real parmana-phinite-agent integration as the worked example.

<Info>
  Repo copy of this guide: `docs/connectors/CONNECTING_AN_AGENT.md`, kept in
  sync with this page. Covers the caller/agent side only. For what happens after
  `APPROVED` (Execution Gateway → connector → the real business system), see
  [Add a connector](/guides/add-a-connector) and
  `docs/connectors/PAYTM_CONNECTOR.md` (repo root).
</Info>

## The mental model

```text theme={null}
AI Agent  --------------------------------------------------->  Parmana
   |  understands the request, extracts an intent                  |
   |  never decides whether the action is authorized                |
   v                                                                v
"I want to refund order X for ₹500"                    caller auth -> capability check
                                                         -> CapabilityPolicyBinder
                                                         -> SignalIntentBinder
                                                         -> PolicyEngine.evaluate
                                                         -> APPROVE or REJECT (binary, signed)
```

The agent proposes an intent. Parmana decides whether it's authorized. Only Parmana's decision can
unlock execution. If your integration ever lets the agent set a value that determines the outcome —
an approval flag, a fraud-check result, a policy name — the model is broken, even if it "works."

## What you need, and why

| # | You need                                                                           | Why                                                                                                                                                         |
| - | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | A reachable Parmana deployment                                                     | Nothing to connect to without one.                                                                                                                          |
| 2 | A capability name (e.g. `paytm:refund`)                                            | Parmana authorizes capabilities, not free-form actions. Exact string matching — `refund` ≠ `paytm:refund`.                                                  |
| 3 | A deployed policy bound to that capability                                         | Decides APPROVE/REJECT. Missing → `404 PolicyNotFoundError`.                                                                                                |
| 4 | The policy's exact `signalsSchema`/`boundSignals`                                  | You must send every referenced signal; any `boundSignals` entry must equal the real `intent.parameters` value or the request is rejected before evaluation. |
| 5 | An API key scoped to exactly that capability                                       | Your agent's identity — see below.                                                                                                                          |
| 6 | A trusted-signal source that is not the conversation                               | Signals must come from an independent business system, never inferred from what the customer said.                                                          |
| 7 | Real UUID generation                                                               | `businessTransactionId` is checked against a UUID-shaped regex (versions 1–5); anything else is a `400` before any other logic runs.                        |
| 8 | (For real execution) a registered connector for your capability on this deployment | `APPROVED` ≠ "executed." A separate, verifiable claim — see step 8 below.                                                                                   |

## Step by step

**1. Read the policy file directly.** `cat policies/customer-refund/1.0.0/policy.json` — don't guess
field names from a description.

**2. Pick the exact capability string.** Case- and character-exact. This single detail is the most
common integration failure in practice.

**3. Mint a scoped API key:**

```bash theme={null}
npx tsx scripts/generate-api-key.ts \
  --caller-id my-refund-agent \
  --allowed-capabilities "paytm:refund" \
  --credential-holder-type SERVICE
```

Use `SERVICE`, never `"AGENT"` — that value doesn't exist in `AuthorityType`. Never grant `"*"` to a
single-purpose agent; `/callers/me`'s `unrestrictedCapabilities` is literally
`allowedCapabilities.includes("*")`.

<Warning>
  **If you're the deployment operator adding this key** (not just the agent
  developer): `PARMANA_API_KEYS` is one environment variable holding a JSON
  **array**. Two real mistakes were made hand-editing a live deployment's value
  in a hosting dashboard: pasting the new entry as the *entire* value instead of
  appending it to the array (every caller gets a 500 until fixed — the server
  fails closed on malformed JSON), and losing existing entries by replacing
  rather than appending. Always paste the complete array — every existing entry
  plus the new one. Full account:
  `docs/operations/2026-09-15-kms-migration-troubleshooting-guide.md`.
</Warning>

**4. Verify the key before writing agent code:**

```bash theme={null}
curl -i "https://<deployment>/callers/me" -H "Authorization: Bearer <key>"
```

If this fails, fix authentication first — nothing about your agent's request shape is relevant yet.

**5. Wire up real trusted-signal sources** for every field in the policy's `signalsSchema`. Not a
placeholder to fix later — from day one.

**6. Build the request:**

<Warning>
  **This example was missing `metadata` until 2026-09-14** — found while running
  [End-to-end: agent → Parmana → Paytm](/guides/end-to-end-paytm-flow) against a real
  deployment. Omitting it fails with
  `{"error":"metadata.businessTransactionId must match businessTransactionId."}` (400),
  before policy is ever evaluated. It's now included below.
</Warning>

```json theme={null}
{
  "businessTransactionId": "<uuid v4>",
  "metadata": {
    "businessTransactionId": "<same uuid v4 as above>"
  },
  "authority": {
    "authorityId": "<uuid>",
    "authorityType": "SERVICE",
    "principalId": "my-refund-agent",
    "issuedAt": "<iso8601>"
  },
  "authorization": {
    "authorizationId": "<uuid>",
    "authorityId": "<uuid>",
    "purpose": "Authorize Paytm customer refund",
    "issuedAt": "<iso8601>"
  },
  "intent": {
    "intentId": "<uuid>",
    "authorizationId": "<uuid>",
    "action": "paytm:refund",
    "target": "<orderId>",
    "parameters": {
      "orderId": "<orderId>",
      "transactionId": "<txnId>",
      "amount": 500
    },
    "createdAt": "<iso8601>"
  },
  "policy": {
    "name": "customer-refund",
    "version": "1.0.0",
    "schemaVersion": "1.0.0"
  },
  "signals": {
    "refundEligible": true,
    "managerApproved": true,
    "fraudCheckPassed": true,
    "refundAmount": 500
  },
  "status": "RECEIVED"
}
```

`policy.name`/`policy.version` is never inferred from `intent.action` — name it explicitly, or a
caller could pair a real capability with an unrelated policy.

**7. Send it, handle the response** — see the complete reference table below.

**8. Before claiming real execution**, confirm a connector is actually registered for your
capability on this deployment. `docs/site/guides/live-api-and-demos.mdx` documents that the
general-purpose demo deployment historically had **no connector registered at all**. "Got APPROVED"
and "the action executed" are two separately-verifiable claims — never conflate them.

**9. Verify independently**: `GET /refusal/:id` + `POST /refusal/verify` for a rejection,
`GET /trust-records/:id` (only once a connector is wired) for an execution, or fully offline with
`verifyExecutionTrustRecordOffline`.

## Complete response reference

Every row is cited to the exact source producing it:

| Status | `code`                   | Source                                                                                                                                                              | Meaning                                                                                                                                                                                                  | Do this                                                                                                                                                                                                                                  |
| ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | —                        | Decision `APPROVE`                                                                                                                                                  | Authorized; a signed decision exists. Dispatch (if any) happens after.                                                                                                                                   | Report authorization only — not execution, unless separately confirmed.                                                                                                                                                                  |
| `400`  | —                        | Invalid `businessTransactionId` / body shape                                                                                                                        | Structural rejection, before auth or policy.                                                                                                                                                             | Fix request construction.                                                                                                                                                                                                                |
| `403`  | (none)                   | `principalId` not permitted                                                                                                                                         | Caller can't assert that principal.                                                                                                                                                                      | Use your own `callerId`.                                                                                                                                                                                                                 |
| `403`  | `CAPABILITY_NOT_ALLOWED` | `intent.action` not in your key's `allowedCapabilities`                                                                                                             | **Most common real-world bug.** Exact-string mismatch.                                                                                                                                                   | Compare `/callers/me`'s `allowedCapabilities` to your `intent.action`, character for character.                                                                                                                                          |
| `403`  | `POLICY_DENIED`          | `ExecutionGate.enforce` — uniform for *every* rejection cause                                                                                                       | A real, correct decision. `message` names the actual rule that matched (excessive amount, failed fraud check, etc.).                                                                                     | Respect it. Never retry with altered parameters.                                                                                                                                                                                         |
| `404`  | —                        | `PolicyNotFoundError`                                                                                                                                               | `policy.name`/`version` not deployed on this instance.                                                                                                                                                   | Check spelling/version and confirm deployment.                                                                                                                                                                                           |
| `409`  | —                        | `DuplicateBusinessTransactionError`                                                                                                                                 | Reused `businessTransactionId`.                                                                                                                                                                          | Fresh UUID per attempt; design idempotency separately if needed.                                                                                                                                                                         |
| `500`  | —                        | Generic fallback — catches an unregistered-connector error **and** `ExecutionGateway` rejecting a signed envelope (bad signature, tampered content, replayed nonce) | Deliberately ambiguous by design, not a flaw in the error handler. Very often "APPROVED, but no connector registered" — or a Parmana-side signing/verification bug with nothing to do with your request. | Check server logs for the exact message before assuming it's your bug — `docs/operations/2026-09-15-kms-migration-troubleshooting-guide.md` (repo root) documents a real example of this exact failure mode caused entirely server-side. |
| (none) | —                        | Timeout/network                                                                                                                                                     | Unknown whether Parmana ever received it.                                                                                                                                                                | Treat as unresolved — never guess APPROVED or REJECTED.                                                                                                                                                                                  |

## Common mistakes

* Capability string mismatch between `intent.action` and your key's `allowedCapabilities`.
* `authorityType: "AGENT"` — invalid; use `"SERVICE"`.
* A signal inferred from the customer's words instead of an independent business system.
* Treating `POLICY_DENIED` as a bug instead of a correct decision.
* Claiming execution from `APPROVED` alone, without confirming connector registration.
* Granting `"*"` "to get it working" and never narrowing it.
* Hand-editing `PARMANA_API_KEYS` by pasting a single entry instead of the complete array.
* Assuming a `500` always means your request was wrong — it's deliberately ambiguous by design.

## Reference implementation

`pavancharak/parmana-phinite-agent` is a real, working implementation of every step above. Two bugs
were found and fixed in it: the exact `CAPABILITY_NOT_ALLOWED` mistake from the table above
(`"refund"` instead of `"paytm:refund"`), and error handling that crashed instead of resolving
unrecognized failures to an unresolved client-side state. See that repository's README for the full
writeup.

**Second round, entirely on Parmana's own side (2026-09-15/16):** after those agent-side fixes, this
same integration hit a sequence of purely Parmana-operator issues onboarding as a new caller on a
freshly KMS-migrated deployment: no `PARMANA_API_KEYS` entry at all (401) → a malformed hand-edited
entry (500 for every caller) → no `allowedPrincipalIds` grant for the asserted principal (403) → a
genuine Parmana-side bug where the gateway's signing and verification paths silently used different
keys after a KMS migration (500, unrelated to the request itself —
`examples/tutorials/114-signing-verification-key-agreement/` reproduces it). None of these were agent
bugs. Full account: `docs/operations/2026-09-15-kms-migration-troubleshooting-guide.md`.

## Next

<CardGroup cols={2}>
  <Card title="Add a connector" icon="plug" href="/guides/add-a-connector">
    What happens after APPROVED: Execution Gateway, connector dispatch,
    execution evidence.
  </Card>

  <Card title="Live API and Demos" icon="server" href="/guides/live-api-and-demos">
    Call the real, deployed Parmana API directly and see what's and isn't wired
    on it today.
  </Card>
</CardGroup>
