This page connects a system Parmana has no built in connector for, such as your ERP, CRM or an internal service, with
no change to Parmana’s code (ADR-0013). You run a small HTTPS endpoint in front of the system. Parmana releases every
approved request for the action to that endpoint as a signed release; the endpoint checks it with an SDK helper,
acts with its own credentials, and answers. Parmana holds none of the system’s credentials.
Status, 2026-10-01. Registering a connector through maker checker is live
in production (docs/CLAIMS.md 2.49). Releasing to a registered endpoint is
built and tested but not yet checked against a live endpoint (ADR-0013
step 6), and the SDK helpers verifyParmanaRelease and
verify_parmana_release are in the repository but not yet published to
npm and PyPI. Until the next SDK release, build the endpoint from the
repository’s SDK packages.
Follow it literally, as agents/integrate is followed. If your situation is not covered, stop and ask the operator.
Rules that apply to every step
- Do the steps in order. Do not skip one.
- At each step, compare what you received with Expect. If it differs, follow If not and stop.
- Never invent a value. Every input comes from the table below or from a response.
- The endpoint acts only on what a verified release names. It never acts on a request that did not come from Parmana.
- Two different people propose and approve every change. One person holding both keys defeats the control.
Step 1: check the server
Do: GET {PARMANA_URL}/health and GET {PARMANA_URL}/ready.
Expect: 200 {"status":"UP"}, and 200 with "status":"READY" and "authDisabled":false.
If not: stop. See Integrate Parmana, step 1.
Step 2: write the policy
Do: write POLICY_NAME at POLICY_VERSION. Every policy must require a signed approval: declare one approval
signal in approvalSignals, name the resource it approves ("target" or a path into parameters), and make every
approve rule require that signal with is_true. A fact the caller declares can refuse, never authorize: give each one a
reason in unboundSignalReasons.
With "value": "parameters.amount", an approval also caps the amount. Leave value out if the action has no amount.
Expect: a JSON file. Step 3 validates it on the server.
Step 3: propose the policy (maker)
Do: with PROPOSER_KEY, POST {PARMANA_URL}/policies/{POLICY_NAME}/{POLICY_VERSION}/pending-changes with
{"reason": "...", "proposedContent": <the policy file, unchanged>}. The exact commands, in Bash and PowerShell, are in
Policy lifecycle and approvals, step 3.
Expect: 201 with a pendingPolicyChangeId and "status":"PENDING_APPROVAL".
If not:
400: the policy is refused, and the message says why (for example, an approve rule that does not require the
approval signal). Fix the file, then repeat this step.
403 NON_HUMAN_CALLER_DENIED: PROPOSER_KEY is not a human key. Stop. Ask the operator.
409 CONFLICT: a proposal for this version is already open. Approve or reject it first.
Step 4: approve the policy (checker)
Do: the second person reads the proposed content (GET /policies/pending-changes?status=PENDING_APPROVAL), signs a
step up authorization for that change id and action approve on their own machine, and posts it to
/policies/pending-changes/{pendingPolicyChangeId}/approve with APPROVER_KEY. Commands:
Policy lifecycle and approvals, step 5.
Expect: 200 and "status":"APPROVED". This version is now the version in effect for POLICY_NAME.
If not: 403 SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE (the proposer’s key was used: use the second person’s) or
403 STEP_UP_AUTHORIZATION_INVALID (signed for another change or action, expired after 120 seconds, or used before: sign
again). Then repeat this step.
Step 5: run the endpoint
Do: deploy the example endpoint for your language, with act replaced by the call into your system:
- TypeScript:
typescript/examples/07-external-connector-endpoint.ts, createReleaseHandler({ publicKeys, audience, act }).
- Python:
python/examples/13_external_connector_endpoint.py, create_release_handler(public_keys=..., audience=..., act=...).
Give it Parmana’s public key (GET {PARMANA_URL}/keys/default, or client.publicKey("default")) and ENDPOINT_URL
exactly as you will register it. Serve it over HTTPS at ENDPOINT_URL. Keep its record of answers by
businessTransactionId in durable storage, not in memory: Parmana may send the same release again after a timeout, and
the endpoint must answer with its first result, not act twice.
Before it acts, the helper checks, in order: the body’s shape, the signature over the canonical release with the key
named in signature.keyId, that release.audience is this endpoint, and that the release has not expired (60 seconds
after it was issued, with 30 seconds of clock skew). It acts only on capability, target and parameters from the
verified release, and answers 200 with:
Expect: POST {ENDPOINT_URL} with body {} answers 401 with
{"errors":["the body is not { release, signature }"]}. An endpoint that acts on an unsigned body is wrong.
If not: do not register it. Fix the endpoint first.
Step 6: propose the registration (maker)
Do: with PROPOSER_KEY:
timeoutMs is optional: 1000 to 30000, default 10000.
Expect: 201 with a changeId, "status":"PENDING_APPROVAL", and endpointUrl as stored (the host in lowercase).
Use that stored endpointUrl as the endpoint’s audience in step 5.
If not:
400 INVALID_EXTERNAL_CONNECTOR: a field is malformed, or the namespace belongs to a built in connector. The message
names the field.
400 EXTERNAL_ENDPOINT_ADDRESS_REFUSED: the endpoint is not https, names an IP address or localhost, does not
resolve, or resolves to an address that is not public. Fix the host.
409 CONFLICT: the capability already has an active registration (revoke it first, see below), or a change for it
is already pending.
Step 7: approve the registration (checker)
Do: the second person reviews it (GET /external-connectors/changes?status=PENDING_APPROVAL), then signs a step up
authorization for the changeId and action approve. It is the same script as for a policy change: the change id goes
in --pending-policy-change-id.
Expect: 200 and "status":"APPROVED". GET /external-connectors lists the capability with "status":"active".
If not:
400 EXTERNAL_ENDPOINT_ADDRESS_REFUSED: the host now resolves to an address that is not public. The address is
checked again at approval. Fix the DNS or reject the change.
403 (SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE or STEP_UP_AUTHORIZATION_INVALID): as in step 4.
409 CONFLICT: the change was already resolved, or another registration became active.
Step 8: check the policy in effect for the capability
Do: GET {PARMANA_URL}/policies/in-effect?capability={CAPABILITY} with any human key or the agent’s key.
Expect: 200 with "policy": {"name": POLICY_NAME, "version": POLICY_VERSION, "schemaVersion": "1.0.0"} and
signals.approval naming your approval signal.
If not:
409 NO_APPROVED_POLICY_VERSION: no version of POLICY_NAME is approved. Do steps 3 and 4. Every request is refused
until then.
404 CAPABILITY_NOT_BOUND: the registration is not active. Check step 7.
503 POLICY_VERSION_UNAVAILABLE: the server could not read it. Try again; if it persists, tell the operator.
Step 9: grant the capability to the agent’s key (operator)
Do: add CAPABILITY to allowedCapabilities of the agent’s entry in the server’s API keys, keeping every other
entry, and deploy. See the warning in Connect an agent, step 3.
Expect: GET {PARMANA_URL}/callers/me with the agent’s key lists CAPABILITY in allowedCapabilities.
If not: the key was not updated or the deploy has not finished. Do not grant "*" instead.
Step 10: make sure an approver is trusted
Do: GET {PARMANA_URL}/approval-issuers with a human key.
Expect: the action approver’s approverId and keyId, with "revoked": false.
If not: add the approver’s key through maker checker: Manage approvers.
Step 11: send requests
The agent now follows Integrate Parmana: specification for AI agents from step 3, with
ACTION = CAPABILITY. Nothing in that procedure differs for an external connector.
The first request for a resource is refused until the approver signs an approval for it:
The agent sends a new request with the approval signal true and approval.json in signals.approvalArtifact.
Expect: 200 with an Execution Trust Record. The endpoint received one release naming the capability, the target,
the parameters, the policy at its version, the approver, the authorization id and the endpoint as audience, and acted
once. The endpoint’s answer is recorded in the record as its claim.
If not:
503 CONNECTOR_NOT_REGISTERED: there is no active registration for the capability. Nothing was sent. Check step 7.
502 EXECUTION_OUTCOME_UNKNOWN: the release failed. The endpoint may have acted: it answered something other than
200, timed out, answered without echoing businessTransactionId and capability, or its result was not an object
of at most 16 KB. A request naming a parameter outside PARAMETERS, or an endpoint whose host now resolves to a non
public address, is also reported this way, although nothing was sent. Do not resend as a new transaction. Check
the endpoint’s own record, then close the Execution Intent with
POST /execution-intents/{businessTransactionId}/resolve.
- Any refusal before release (
403, 400): as in Integrate Parmana, step 6.
Change or remove a connector
- Move to a new endpoint: propose
{"action":"revoke","capability":"...","reason":"..."}, approve it (step 7), then
register the new endpoint (steps 6 and 7). From the approved revoke until the new registration is approved, requests
are refused with 503 CONNECTOR_NOT_REGISTERED.
- Change the policy: propose and approve a new version of
POLICY_NAME (steps 3 and 4). It takes effect at once;
the registration does not change.
- Stop it: revoke it. Nothing is deleted:
GET /external-connectors keeps listing it as revoked, so every release
ever made to it stays explainable.
Rules that are never broken
- Parmana releases only to an
https host that resolves only to public addresses, checked when the registration is
proposed, when it is approved, and at every release. It connects to the address it checked and follows no redirect.
- A release names one capability, target and parameter set a person approved, is addressed to one endpoint, and expires
60 seconds after it is issued. No shared secret is involved: the signature is the authentication.
- What the endpoint answers is its claim, not proof that it acted (
docs/VERIFICATION-GAPS.md G-82).
See it run
- Tutorial 123:
npx tsx examples/tutorials/123-external-connector/run.ts registers an ERP endpoint through maker
checker, releases to the TypeScript example endpoint, answers a retry once, refuses a release for another endpoint,
and stops at a revoke.
- The
/external-connectors operations in the API reference,
with real responses for every refusal.
- The design and its security reasoning:
docs/adr/ADR-0013-Generic-External-Connector.md.