GET /version reports an internal build identifier, not a
product version). The SDKs are versioned independently on their own registries, see
TypeScript SDK and Python SDK for their current versions
and per-version notes. This page covers changes that affect what you can build against, not
every commit, run git log in the repository for the complete record.
2026-10-11
Added: live checks for authority grants, business signal sources and a real refundexamples/live-checksgains three procedures for production. Theauthority-grantandbusiness-signal-sourcechecks passed in production the same day, all four requests each as required; the refund check is ready, not yet run:authority-grant(a grant decides: refused before it, approved within its limit, refused over it and after its revoke),business-signal-source(a deployed demo source answers, signed; the release carries its answer as a condition) andpaytm-refund(one real 1 INR refund through the rotated credentials, its record verified offline).examples/live-checks/governance.tsruns the maker checker steps for policies, connectors, sources and grants with the SDK, showing each change and asking the checker to typeAPPROVEbefore signing the step up.
@parmana/sdkandparmana1.6.0 add the business signal source and authority grant methods, andconditionsonParmanaRelease.@parmana/connector-sdk0.3.0 addsReleaseCondition.parmana-connector-sdkis unchanged at 0.2.0.- Published on npm and PyPI from the files of release sdk-v1.6.0, byte for byte: see New in 1.6.0.
- The authorization Parmana signs now carries what the decision rests on: the business facts with their
validUntil, and the agent’s authority grant. The gateway checks them again at release; if one no longer holds, nothing is sent and the request gets409 AUTHORIZATION_NO_LONGER_VALIDwithexecutionStatus: NOT_EXECUTED. - A release to an external connector carries the facts as signed
conditionsand expires no later than the earliest one. An endpoint that finds one no longer holds answersconditionsNotMetand does not act. See Connect any external system. 502 EXECUTION_OUTCOME_UNKNOWNnow also saysexecutionStatus: EXECUTION_UNKNOWN.- The SDKs’
ParmanaReleasetype hasconditions; the example endpoints take aconditionsHoldcheck.
- Say which agent may have which action decided, within limits on each request (
min,max,oneOfby Intent path), until when:POST /authority-grants/changes, approved by a second person with a step up authorization. No one grants authority to themselves. See Grant an agent authority. - A policy opts in with
requireAuthorityGrant. A request from a key with no grant in force, or outside its limits, is refused asNOT_AUTHORIZED; after the grant’s expiry, asAUTHORITY_EXPIRED. The grant checked is in the signed record, underassessment.authority.grant. GET /policies/in-effectanswersauthorityGrantRequired: truefor such a policy.- Self managed deployments: apply the new migration with
npm run db:migrate -- applybefore deploying this version. - No shipped policy requires a grant; decisions are otherwise unchanged.
- Register a business system that owns facts a policy needs, an ERP, an order system, a CRM, a ledger, under a name,
through maker checker:
POST /business-signal-sources/changes, approved by a second person with a step up authorization. See Connect a business signal source. - Parmana asks it with a query signed by its default key, and, when the source is registered with an Ed25519 key,
accepts only answers signed with that key. The Trusted Signal records
sourceProof,sourceKeyIdandsourceProofVerified. An answer Parmana cannot trust refuses the request asSOURCE_UNAVAILABLE. typescript/examples/08-business-signal-source.tsis a complete source endpoint.- Self managed deployments: apply the new migration with
npm run db:migrate -- applybefore deploying this version. - The SDK methods (
proposeBusinessSignalSourceChange(),business_signal_sources.propose_register()and the rest) are in SDK 1.6.0.
2026-10-10
Added: business validation, in the runtime- A policy can name the business system that owns a fact (
signalSources). Parmana asks that system itself, never using the agent’s value, records the answer as a Trusted Signal, and evaluates the policy on it. A proposed value that differs from the source’s is refused. See Business validation. - Every decision now carries
assessment: authority, business validation and, for a refusal, execution, as separate statuses, signed inside the Execution Trust Record and the Refusal Record. Records written before this change have noassessmentand still verify. GET /policies/in-effectlists a policy’s sourced facts undersignals.sourced, when it has any.- No business system is registered in the server yet, so a policy that declares one is refused as
SOURCE_UNAVAILABLE. No shipped policy declares one; decisions are otherwise unchanged.
2026-10-06
Added: policy content hash in Refusal Records- A Refusal Record now carries
policyContentHash, the SHA-256 hash of the policy content that decided the refusal, inside the signed record. It is the same value an Execution Trust Record carries intransaction.policy.contentHash, so a refusal and an approval under the same policy can be matched. See Refusal Records. - Records written before this change have no
policyContentHashand still verify. - Self managed deployments: apply the new migration with
npm run db:migrate -- applybefore deploying this version. The Docker Compose deployment applies it on start.
- Each GitHub release now builds the API server from the repository’s
Dockerfileand publishes it asghcr.io/pavancharak/parmana-api:<release tag>, with SLSA build provenance attached to the image in the registry. See Verifying the server image. - The provenance covers that image only. The hosted API and the public sandbox are built by Vercel and carry none.
- Technical paper: Parmana’s design, threat model, evaluation (attack scenarios, mutation testing, fuzzing, static analysis) and limitations, with a PDF.
- Talk and demo videos: the outline of a 20 minute talk, and step by step scripts for four demo videos against the public sandbox.
- Case studies applies Parmana to three publicly reported incidents (Replit, Amazon Q, Air Canada): what happened, with sources, and what Parmana would and would not have changed.
- Questions and ideas now go to GitHub Discussions, with forms for questions, ideas and show and tell; the new issue page links there and to private security reporting.
- Published GHSA-hp43-9p54-wqp9
(low): a policy name or version of
.or..let the file based policy store read and write apolicy.jsonone directory outside the policy directory. Fixed on 2026-10-05; only deployments withPARMANA_STORAGEset tomemoryor unset were affected. POST /policies/{name}/{version}/pending-changesnow refuses a dot-only name, version orproposedContent.policyVersionwith a 400 when the change is proposed, not when it is applied.@parmana/policyis 0.1.1.
- How Parmana compares sets Parmana next to IAM, API gateways, LLM guardrails, policy engines and agent framework confirmations: what each answers, what Parmana adds, where Parmana is weaker, and how to use them together.
- Regulation mapping maps Parmana to the EU AI Act’s human oversight and record keeping provisions (Articles 12, 14 and 26), the NIST AI RMF (GOVERN 3.2, MAP 3.5, MANAGE 2.4, MEASURE 2.7 and 2.8, MANAGE 4.1), ISO/IEC 42001 Annex A and the Reserve Bank of India’s IT Governance Master Direction, FREE-AI report and draft model risk management guidance, with the evidence for each and what stays with your organization.
- OWASP mapping takes each risk in the OWASP Top 10 for LLM Applications 2026 and the OWASP Top 10 for Agentic Applications 2026 and states what Parmana does about it, rated covered, partly or not covered, with the claim, threat and attack scenario that show it, and what is left to you.
400, not a 500
- A
POST /executeorPOST /transactionsbody withoutmetadata,authority,authorization,intentorpolicy, or with one of themnullor of the wrong type, or a required field that is not a string, is refused with400naming it (G-87). Before, a missingpolicyorauthorizationwas a bare500. Nothing was executed either way. See Error handling.
- A release to an external connector whose first resolved address refuses the connection now tries the endpoint’s other checked addresses, under one timeout (G-83). Once a connection is made nothing is retried, so the endpoint never receives a release twice.
- Property based tests of the request boundary, policy loading and evaluation, the offline verifier and approval
parsing run with every
npm test. Besides G-87 they found that the policy validator threw aTypeErroron fields of the wrong type (G-90) and that the offline verifier threw on malformed hybrid signatures (G-91). Both now refuse with a clear error.
- Limitations lists every gap still open and what to do meanwhile; the same
list, with evidence, is under “Open issues at a glance” in
docs/VERIFICATION-GAPS.md.
2026-10-05
Changed: a request whose signals do not have the declared types is refused- Every signal a policy declares in
signalsSchemamust now have that type (boolean,numberorstring) before any rule runs. A mismatch, such as an amount sent as"500", is refused withmatchedRuleIdsignal-type-violationand a reason naming the signal. Signals the policy does not declare, and signals not sent, are not affected. A policy whosesignalsSchemauses any other type no longer validates.
- Verifying an Execution Trust Record now fails when an
Executioncarries only some of its chain fields (chainHash,chainSignature,previousChainHash), or has none after a chained one. Before, such anExecutionwas skipped as data from before chaining, so an edited one with itschainSignatureremoved passed the chain check; the record’s own signature still caught the edit. Records the runtime writes are unaffected: it always writes both fields together.docs/VERIFICATION-GAPS.mdG-89.
npm run mutation -- <package>measures how many deliberate changes to the security code the tests catch. First scores:envelope-verifier89.7%,approval77.7%,policy50.3%,execution-gateway67.6%,crypto65.4% (on Node 22, which skips the ML-DSA-65 tests; the command now needs Node 24). Re-run on Node 24 after the fixes:envelope-verifier94.0%,approval93.9%,policy96.0%,crypto85.0%,execution-gateway83.6%. The gaps that matter for security now have tests; see docs/MUTATION-TESTING.md.
- From the next release on, each SDK package on a
GitHub release has a CycloneDX SBOM beside it
(
<package file>.cdx.json), covered by the signed provenance. See Verifying an SDK package.
- Verify a record in your browser checks a signed Execution Trust Record’s hash and Ed25519 signature with Web Crypto, in the reader’s browser. It comes with an example record and a changed copy that fails, and can fetch the sandbox’s public key. A test runs the page’s code against records signed by Parmana’s signer.
- THREAT-MODEL.md lists the assets, trust boundaries and attackers, and for each of 17 threats the control, the evidence (an attack scenario or a claim) and the residual risk, plus the assumptions a review should state.
- SECURITY-CHALLENGE.md lists what counts as breaking Parmana on a copy you run yourself, what does not, and how a confirmed break is reported and credited. Local copies only; the hosted API and the sandbox are out of scope.
npm run evaluate
- Runs 16 attack scenarios against the code (replay, forgery, acting without a signed approval, policy substitution, gateway bypass, record tampering and more) and reports each one blocked or not, with a report tied to the commit. See Run the attack scenarios.
- Claims and evidence now gives, for each claim, the command
that reproduces its evidence, and
npm run evaluate -- EV-04runs a single scenario.
- Each SDK release on GitHub carries the four packages built by GitHub Actions from the release’s tag, and signed SLSA provenance recording the commit and workflow that built each one. See Verifying an SDK package. The first is sdk-v1.5.0-r1, with the current SDK versions.
- Your first governed action now asks for Node.js 24.6 or later, which the repository requires; it said 22.
2026-10-04
Changed: the SDKs are licensed under the Apache License 2.0@parmana/sdk,parmana,@parmana/connector-sdkandparmana-connector-sdkare licensed under the Apache License 2.0 again, replacing the proprietary license announced on 2026-10-03. The Parmana server stays proprietary, evaluation only. See License and evaluation.- Published:
@parmana/sdkandparmana1.5.0, with the new methods and fixes and the new license, and@parmana/connector-sdkandparmana-connector-sdk0.2.0, with the connector types added since 0.1.0 and the new license.
2026-10-03
Added: evaluation and audit pages- New Evaluate and audit section: an evaluation guide, an Audit guide that names the enforcement points, what an attacker controls in each case and a bounded scenario, and a Claims and evidence map from each core claim to its tests.
- A rejected or already approved policy change can no longer be approved and applied
(
409). Two approvals of the same change at once apply it only once; a retry by the same approver after a partial failure completes. New migration20261003120000_unique_policy_change_approval_per_pending_change.sql. POST /executerefuses ametadata.tenantIdthe API key does not list in its newallowedTenantIds(403 TENANT_NOT_ALLOWED). A request with no tenant id is unaffected.- A signed approval’s
scope.fieldis checked:valuewhen the policy declares avalue, otherwiseresourceId. The SDKs andscripts/sign-approval.tsalready write these. - The gateway checks a request with no
signalsagainst the authorization’s signed signals hash instead of skipping the check. - Failed authentication is rate limited per IP (
RATE_LIMIT_AUTH_FAILURE_PER_MINUTE, default 30), and key discovery shares the public rate limiter. pageandpageSizeonGET /transactionsandGET /trust-recordsmust be whole numbers;pageSizeis at most 100 (400otherwise).- Under AWS KMS:
GET /.well-known/jwks.jsonlists the signing keys instead of answering501; a key ARN is refused as a key id; public keys are cached for 5 minutes; a tenant keytenant.<id>maps to the aliasalias/tenant/<id>. - The server refuses to start with
CRYPTO_MODE=hybridandKEY_PROVIDER=aws-kms, or with hybrid key files missing. - GitHub targets accept only a valid
owner/repo#number.
- From the next release,
@parmana/sdkandparmanaare licensed under the same proprietary, evaluation only license as the rest of the repository, replacing the Apache License 2.0. Versions already published keep the license they were published under.@parmana/signstays under the Apache License 2.0.
- The source moved from
pavancharak/AgentLabsBuildathontopavancharak/parmana, with its full history. Every link in these docs now points to the new repository. Links to older pull requests and issues still open in the old repository.
@parmana/sign 0.2.0)
@parmana/sign0.2.0 is on npm. It verifies Execution Trust Records and Execution Intents offline, with only the record and the public keys, including hybrid (Ed25519 and ML-DSA-65) records and large records signed through AWS KMS.- New Verification SDK tab: overview and quick start, public keys, a reference page for each verifier and for the signing primitives, and every error message with what to do.
2026-10-02
Fixed: canonical JSON keeps"__proto__" keys
- The server’s canonical serializer and the TypeScript SDK’s
canonicalSerializesilently dropped an object key named"__proto__", so its content was not covered by the hash or signature. Both now keep it, matching the Python SDK and@parmana/sign. Output for data without that key is unchanged, so existing signatures stay valid.
- Every request and record visitors had sent to the public sandbox was deleted around 03:30 UTC, around the retention job’s first run. Production was not affected. The job is paused. Sandbox records are not guaranteed to be kept: keep any record you need.
- The retention function now refuses any period under 7 days without an explicit confirmation word, and logs every run that deletes.
- A daily job in the sandbox database deletes every request and record visitors sent more than 7 days ago, and keeps the sandbox’s policy, approver and registration. Sandbox only: it is not a migration, and it refuses any database without the sandbox’s registration.
- The sandbox is live at
https://parmana-sandbox.vercel.app: the same code as production, its own database, signing keys and demo approver, one action (sandbox:receipt) released to an endpoint that acts on nothing. Its live check passed all nine checks on 2026-10-02. - A published demo key,
2VfYWCzt_cBAPK-8uufX6ordfY2JuQhFPsohuEumKME(callersandbox-visitor, allowed onlysandbox:receipt). - New Playground page: the whole flow in cURL, PowerShell, TypeScript and Python, each run against the sandbox before publishing, with every real response and every refusal.
- Every Try it panel in the REST API tab now calls the sandbox first, with the demo key filled in. Production is still listed as a server.
- The Quickstart starts with two requests to the sandbox, and Integrate Parmana says how an agent practises there.
- Known issue, Python SDK 1.4.0:
verify_execution_trust_record_offlinereports a hash mismatch when given the decoded model of a record whosepreviousChainHashisnull. Pass the record as plain JSON instead; the TypeScript SDK is not affected. Fixed in the repository the same day for the next Python release: a decoded model keeps the server’s JSON and encodes back to exactly it, which also fixes sending a decoded refusal record or Execution Intent back for verification.
2026-10-01
Added: browser access and sandbox mode, for the public playground (ADR-0014 step 1)PARMANA_CORS_ORIGINS: lets a browser on the listed origins call the API. Unset, as in production today, no CORS header is sent.PARMANA_SANDBOX=truemountsPOST /sandbox/approvals: the sandbox’s demo approver signs an approval forsandbox:receiptonly, for one resource, valid 5 minutes, accepted once. The server refuses to start in sandbox mode while any Paytm, HubSpot, Slack or GitHub variable is set or caller authentication is disabled, and refuses the demo approver variables outside sandbox mode.- Nothing changes in production unless these variables are set. The sandbox was deployed on 2026-10-02 (above).
- Fixed the same day: the startup guard missed
PARMANA_GITHUB_VERCEL_CONNECT_CONNECTOR_ID, which also turns on the GitHub connector, and ignored a connector variable defined as an empty string, which the connector factories treat as set. Both are refused now, and a test reads the variables from the code that registers each connector.
- TypeScript:
externalConnectors(),proposeExternalConnectorChange(),externalConnectorChanges(),approveExternalConnectorChange()andrejectExternalConnectorChange(), also onExternalConnectorApi. - Python:
client.external_connectorswithlist,propose_register,propose_revoke,list_changes,approve_changeandreject_change, plus shortcuts on the client. - In the repository, not yet on npm or PyPI. The API reference samples for the five
/external-connectorsoperations now use them, and Tutorial 123 registers and revokes its connector through them against a real server.
- Every endpoint page has a cURL, TypeScript and Python sample, each checked as code: the cURL body against the request schema, TypeScript by the compiler, Python by mypy.
- Every JSON response, success and error, shows a real response captured from the server, 36 of them new. A test checks each example against its response schema. The only responses without one are the OpenAPI document itself, the handbook PDF and a redirect.
GET /keys/{keyId}documents the JSON error body its 404 returns.- Every operation’s description now says what it does, when to use it and who may call it, and every response says what happened and what to do next. Every parameter is described.
- The list of routes that need no key was incomplete:
GET /openapi.json,GET /api-manifest.json,POST /execution-intents/verifyand the handbook routes were missing. Fixed in the reference and on Authentication.
- An approved request for a capability registered as an external connector is released to the
registered endpoint as a signed release: audience the endpoint URL, a 60 second expiry, the
authorization id, the policy and the approvers, signed with the key at
GET /keys/default. The endpoint’s address is checked again at every release, and redirects are not followed. - The registration’s policy is the binding: a request must declare it at the version approved
through policy governance, and
GET /policies/in-effect?capability=answers for the capability. Every request is refused while no version of that policy is approved. - Checked live in production on 2026-10-01 against a deployed endpoint: refused with no approval,
released and recorded with one, refused when the approval was reused (
docs/CLAIMS.md2.50). The SDK helpersverifyParmanaReleaseandverify_parmana_releaseare not published yet. - Found in the live check: an approved request took 24 to 32 seconds in production, longer than the
SDKs’ default 30 second timeout (
docs/VERIFICATION-GAPS.mdG-84). After a timeout, read the record before sending anything new. - Examples: an endpoint in TypeScript (
typescript/examples/07-external-connector-endpoint.ts) and in Python (python/examples/13_external_connector_endpoint.py), each checking releases with its SDK helper, and Tutorial 123, which registers the TypeScript one through maker checker and releases to it. The API reference shows real responses for every refusal of the/external-connectorsroutes.
2026-09-30
Added: external connectors, first step (ADR-0013) The goal: connect a new external system to Parmana with no change to Parmana’s code. An operator registers a capability to an HTTPS endpoint they run, and Parmana releases each approved request there as a signed release. Built in steps; only the first is live.- Live in production: registering an external connector through maker checker, like approver
keys. One human caller proposes at
POST /external-connectors/changes(a capability, the endpoint, the policy that governs it, the parameters Parmana may forward, a timeout); a different human caller approves with a step up signature.GET /external-connectorslists every registration. Refused: a capability in a built in connector’s namespace, and an endpoint that is not https, names an IP address or localhost, or resolves to an address that is not public (checked when proposed and again when approved). See the API reference. - Not yet in effect: a registered connector receives no release yet. The adapter that sends
the signed release, and the SDK helpers an endpoint uses to check it
(
verifyParmanaRelease,verify_parmana_release), are merged, but no registration is connected to them yet (the next step), and the helpers are not published yet. A guide to connecting any external system follows when the whole path works.
GET /policies/in-effectnow also returnsdescriptionandsignals: every fact the policy’s rules read (facts), their types (schema), the signals that must equal a value of the Intent (bound), and the signals that need a signed approval with where its resource and amount are in the request (approval). The rule conditions are never returned;descriptionis the policy author’s text for people. An agent no longer needs a copy of the policy, or the operator, to build a request. See Integrate Parmana: specification for AI agents, step 3.- SDK methods, in the next SDK release:
policyInEffect(capability)in TypeScript,policy_in_effect(capability)in Python. Not yet published; 1.4.0 does not have them.
docs/VERIFICATION-GAPS.md G-80 and Human approval.
- Every policy must require a signed approval. A policy with an approve rule that does not
require an
approvalSignalsfact withis_true(as its whole condition or directly inside its top levelall) no longer loads, and cannot be proposed through policy governance. Requests under it are refused with400. - A runtime without an approval verifier refuses every approval instead of trusting the caller’s approval signal.
- Reads need an approval too.
github:pr-fetchis bound togithub-pr-read1.1.0 (readApproved, the pull request).hubspot:deal-fetchhas its own policy,hubspot-deal-read1.0.0 (readApproved, the deal). - Every HubSpot update needs an approval for the deal (
hubspot-deal-update1.1.0,dealUpdateApproved), whatever the amount. The separate pre authorization for large amount changes is no longer used. - Every Slack post needs an approval for the channel (
slack-post-message1.1.0,postApproved), in addition to the channel allowlist. - Reference policies have approval backed versions:
vendor-payment2.1.0,access-control1.1.0,agent-vendor-payment1.1.0,api-key-issuance1.1.0,connector-capability1.1.0,database-change3.1.0,expense-reimbursement1.1.0,production-deployment1.1.0,rag-document-access1.1.0. Older versions stay as history and are refused at load. - Local test approver.
PARMANA_TEST_APPROVER_PUBLIC_KEY_FILEmakes aNODE_ENV=testserver trustlocal-test-approver, so the Quickstart can sign its approval. Set with any otherNODE_ENV, the server refuses to start. - What an integrating agent must do now: attach a signed approval to every GitHub read,
HubSpot read or update, and Slack post, exactly as for refunds and merges. A request without one
is refused with
403 POLICY_DENIED. See Human approval. - Documentation fix. The PowerShell proposal commands in
Policy lifecycle and approvals and
Credentials now read
policy.jsonas UTF-8. Windows PowerShell 5.1Get-Content -Rawreads a file without a byte order mark in the local codepage, so a policy with a character such as an em dash was proposed, and approved, with different text than the file, and CIverify-policy-approvalsthen failed.
2026-09-29
Added-
Approval notifications by email. Set
APPROVAL_EMAIL_TOwith the Resend integration and each request waiting for an approval is emailed to the approver, with exactly what to sign. See Approval notifications. -
Approval notifications. With
APPROVAL_WEBHOOK_URLandAPPROVAL_WEBHOOK_SECRETset, a request refused only for want of a signed approval sends a signedapproval.neededevent to that URL, naming what to sign. Other refusals send nothing. Best effort, and a failed delivery never changes the refusal. See Approval notifications. -
Manage approvers without a deploy. Trust a new approver key, rotate one, or revoke one through the API: one person proposes, a different person approves with a step up signature, and the change applies to the next request. New endpoints under
/approval-issuers, SDK methods in 1.4.0, and the tablesapproval_issuersandapproval_issuer_changes(migration20260929120000_add_approval_issuers.sql, apply before deploying). See Manage approvers.
- Sign an approval of one action on the approver’s own machine, without a clone of the repository:
signApproval()in TypeScript,parmana.crypto.sign_approval()in Python. The result is the same signed approvalscripts/sign-approval.tsmakes, and the server’s own verifier accepts it (tested). See Human approval.
2026-09-28
Security Approved in production the same day:customer-refund 1.2.0, github-pr-approval 1.1.0, github-pr-read 1.0.0 and llm-tool-call 1.1.0. Every refund now needs a signed manager approval.
An AI attack review (an agent manipulated by prompt injection calling the API) found facts that an agent declares, and nothing checks, could authorize real actions. See docs/VERIFICATION-GAPS.md G-73 to G-78.
- A pull request merge needs a signed approval for that pull request (
github-pr-approval1.1.0, G-73). The review, check, branch and risk signals come from the caller and can only refuse a merge now. Reading a pull request (github:pr-fetch) has its own policy,github-pr-read1.0.0 (superseded on 2026-09-30 by 1.1.0, which needs an approval). Both were approved in production the same day. - An older approved policy version is refused once a newer one is approved (G-74), for every policy, not only connector actions. Send the version in effect; approving an older version again rolls back.
- Self hosted quickstart uses
customer-refund1.2.0: its demo refund is refused without a manager approval. The offline check keeps 1.1.0 to prove the authorized path end to end. - Every refund needs a signed manager approval (
customer-refund1.2.0, G-75). Under 1.1.0 a refund up to 10000 was authorized on the agent’s own claim that the order was eligible and passed the fraud check. 1.2.0 also refuses a refund of 0 or less. Approved in production the same day, so it is in effect. Read the version withGET /policies/in-effect?capability=paytm:refund. - Slack posts go only to channels on the server’s allowlist (G-76). Set
SLACK_ALLOWED_CHANNEL_IDS(comma separated channel ids); unset refuses every post. The caller’schannelAuthorizedis no longer trusted, andparameters.channelmust equal the Intent’starget. Applies on deploy. - A connector policy cannot let caller facts alone authorize a write (G-77). A test requires every approve rule of a bound write capability to need a signed approval or a fact the server checks. If you add a connector, classify its capabilities in
connector-policies-not-self-authorizing.test.ts. - Rate limit on the public routes that do work (G-78):
POST /refusal/verify,POST /execution-intents/verify,POST /audit/verifyand/handbookshare a per IP limit,RATE_LIMIT_PUBLIC_PER_MINUTE(default 60). Over it they return429withRetry-After. - An LLM tool call needs a signed human approval for that tool (
llm-tool-call1.1.0, G-74), instead of the caller declaringhumanApproval: true.
GET /policies/in-effect?capability=paytm:refundreturns the policy a request for a capability must declare right now: the bound name and the version most recently approved through policy governance. Agents can read it before sending instead of writing a version into their code, so approving a new version needs no change to the agent. See Get the policy in effect.
-
On the hosted API, a malformed JSON body returned
500; it now returns400 Malformed JSON body.(G-69). -
Paytm refund agent (
parmana-paytm-agent, a separate service): an approved refund could be paid twice (G-70). Its/agent/refundsendpoint asked Parmana to execute the refund and then called Paytm itself as well, while Parmana had already sent the refund to Paytm through its connector. The agent no longer calls Paytm; it returns Paytm’s result from the Trust Record. It also reads the policy version fromGET /policies/in-effectinstead of sending 1.0.0, and can forward a signed manager approval (approvalArtifact). Deployed 2026-09-28; a refused refund was verified in production, and an approved one has not been observed live yet. -
Paytm refunds: more than one refund per transaction, and the reason reaches Paytm (G-71). A
paytm:refundIntent may carryrefundReference, the caller’s own id for one refund: the Paytm refId is then derived from the order, the transaction and that reference, so separate partial refunds of one transaction get separate refIds while retries of one refund keep the same one. Without it, the refId is exactly what it was.refundReasonis now passed to the connector service, which sends it to Paytm as the refund comment. Both are optional (at most 128 and 256 characters). The refund agent (parmana-paytm-agent) sends both since its release of the same day: itsrefIdasrefundReference, itsreasonasrefundReason.
2026-09-27
Added- Signed human approval, declared in the policy, for any action. A policy lists in
approvalSignalswhich of its signals need a signed approval and where the resource (and optionally an amount) is in the request.ApprovalSignalVerifierenforces it for every action, before authorization and again at the gateway, and each approval is used once. Adding an approval to an action is a policy change. See Human approval. - Large refunds use it first. A
paytm:refundabove 10000 runs only with a signed approval from a trusted approver, for that order and covering that amount.managerApproved: truewithout one is refused. Approvers create a key withscripts/generate-approver-key.tsand sign withscripts/sign-approval.ts. No approver is configured by default.
- A live action’s policy version is decided by policy governance. The policy name for each action stays fixed in code; the version is the one most recently approved for that name. A new version takes effect when it is approved, with no deploy, and approving an older version again rolls back. A request naming any other version is refused with a message naming the version in effect, so agents must send that version. See Human approval.
paytm:refundnow usescustomer-refund1.1.0. Up to 10000 is automatic, with nomanagerApprovedneeded; above 10000 and up to 100000 needs a signed manager approval; above 100000 is refused. In production, refunds move to 1.1.0 when it is approved through policy governance.- Approval signatures are verified with Ed25519 only, whatever the server’s own signing algorithm is.
- An approver could not be added on Vercel, because approver public keys were read only from files the platform cannot provide. A trusted approver entry can now carry its public key as
publicKeyPem(G-68). - A valid signed approval (HubSpot amount changes) was always refused at the gateway, because the approval was used up by the first of the two checks. It is now used once, at authorization (G-67).
- Policy lifecycle and approvals. One guide, end to end: write a policy, propose it, a second person approves it, how it takes effect, and how a person signs off on a single action. Commands for Bash and PowerShell. See Policy lifecycle and approvals.
- Human approval. A new page, Human approval, states where people approve what agents do today, how to query refused requests, and what is not available yet: a refused request cannot be approved afterwards, and refunds have no checked manager approval. Recorded as G-65 and G-66 in
docs/VERIFICATION-GAPS.md.
2026-09-25
Added- Self hosted deployment.
docker compose up -d --build --waitruns Parmana and its own Postgres on your infrastructure, with signing keys, API keys, migrations and policies handled, and nothing sent to Parmana. See Self hosted Parmana and the quickstart. - Offline verification.
bash docker/local/offline-check/run.shproves, on a network with no internet route, that policy approval, an authorized and a refused decision, and Trust Record verification with only the public keys all work. See Offline verification. PARMANA_STORAGE=postgres. It selects the same Postgres storage assupabase, which is unchanged. See the environment variable reference.
npm install @parmana/sdk and pip install parmana install 1.3.0. See SDK coverage of the API.
- Policy governance in both SDKs: propose, list for review, approve and reject a policy change, and sign the step up authorization on the approver’s machine (
signPolicyChangeStepUp(),parmana.crypto.sign_policy_change_step_up()). An approver no longer needs a clone of the repository. caller()(GET /callers/me),publicKey()/public_key()(GET /keys/{keyId}) and Trust Record listing (GET /trust-records) in both SDKs.- TypeScript: offline verification of Trust Records and Execution Intents (
verifyExecutionTrustRecordOffline,verifyExecutionIntentOffline), which only the Python SDK had;latestReceipt(); andstatusCodeon every HTTP error, which only the Python SDK had (status_code). - Python: the offline verifiers also accept the SDK’s own models, so
client.execution_intent(id).intentcan be passed directly;latest_receipt()shortcut.
- A request whose action was released and whose connector call then failed (unreachable, timed out, or an error response) now returns
502with codeEXECUTION_OUTCOME_UNKNOWNand a message naming thebusinessTransactionIdandauthorizationId. It used to be a bare500 Internal Server Error. The Execution Intent isERROREDas before: do not resubmit, check the target system and close the intent with resolve. See the error catalog.
npm run db:migrate: tracked migrations for any Postgres.statusandapply --dry-runare read only,applyruns each pending migration once, andbaseline --through <file>records the migrations a database already has without running them. See the production runbook, step 2.
scripts/apply-all-migrations.sqlis safe to run once on an empty database, and not safe to run again on a database that holds data. The docs used to say otherwise. For an existing database, apply only the migration files it does not have yet.
2026-09-21
Added- Execution Intents. Before an action is released to a connector, the runtime now signs and stores a record of
exactly what it is about to release. If that cannot be done, nothing is released and the caller gets
503 EXECUTION_INTENT_UNAVAILABLE. This closes the ordering gap recorded as G-52 and makes the missing record recorded as G-53 repairable. See Execution Intents. - Four routes:
POST /execution-intents/verify(no credential needed),GET /execution-intents/{businessTransactionId},GET /execution-intents/unfinalizedandPOST /execution-intents/{businessTransactionId}/finalize. The last two need a credential provisioned as a verified human. Finalize rebuilds a missing Execution Trust Record for a released action without calling the connector, and is safe to run twice. - A fifth route,
POST /execution-intents/{businessTransactionId}/resolve, lets a verified human close an intent in statePREPAREDorERROREDafter reconciling it at the connector. It records what they found (NOT_EXECUTEDorEXECUTED), a required note, who did it and when, and takes the intent out of the unfinalized list. It is idempotent and never calls a connector. The resolution is an attributed operator statement in unsigned status. It is not tamper evident and it is not a Trust Record. scripts/verify-execution-intent.tsverifies an intent offline with only the public key.- New error codes:
EXECUTION_INTENT_UNAVAILABLE(503),EXECUTION_INTENT_NOT_FOUND(404),EXECUTION_INTENT_RESULT_NOT_RECORDED(409)EXECUTION_INTENTS_NOT_ENABLED(501),EXECUTION_INTENT_NOT_RESOLVABLE(409) andEXECUTION_INTENT_RESOLUTION_INVALID(400). See the error catalog. - The environment variable
EXECUTION_INTENTS_CHECK, which only has an effect intestanddevelopment.
- Execution Intents are enforced by default and cannot be switched off in production. Apply
supabase/migrations/20260921120000_add_execution_intents.sqlbefore you deploy this version. Without the table, every execution is refused with503 EXECUTION_INTENT_UNAVAILABLE. The migration only adds a table and is safe to run twice. GET /readynow also checks that theexecution_intentstable exists, and returns503withNOT_READYand the migration file name when it does not.EXECUTION_RECORD_INCOMPLETEis no longer a dead end. The signed intent exists, and an operator can rebuild the record withPOST /execution-intents/{businessTransactionId}/finalizewhen the intent is in stateRELEASED.- Each released action now costs one more signing operation and three more database writes. Under AWS KMS that is one
more
kms:Sign, about 35 ms in the one measurement made, from a Windows machine toap-south-1.
- A resolution is not tamper evident and does not create a Trust Record. For an action that ran and has no Trust Record, the signed intent, the operator’s note and the connector’s own record are the evidence.
- The SDK methods for the Execution Intent routes are available from SDK 1.2.0, published on 2026-09-21. Earlier versions do not have them. The TypeScript SDK has no offline intent verifier.
- TypeScript
ParmanaClient:executionIntent,verifyExecutionIntent,unfinalizedExecutionIntents,finalizeExecutionIntent,resolveExecutionIntent, with theExecutionIntentApiclass and its types. - Python
ParmanaClient:execution_intent,verify_execution_intent,unfinalized_execution_intents,finalize_execution_intent,resolve_execution_intent, withExecutionIntentApi, the intent models, andparmana.crypto.verify_execution_intent_offline, an offline verifier that needs only the public key.
- The JSON schema
$idvalues underschemas/used the domainschemas.parmana.ai. They now useschemas.parmanasystems.com. They are identifiers only and no route or response changed.
2026-09-20
Changed- The Execution Gateway now fails closed on policy binding. An authorization with no
policyContentHash, a policy that has no signed approval record, a policy whose live content differs from its approved content, or an error from the approval check all reject the request before the connector runs. Authorizations signed beforepolicyContentHashexisted are now rejected instead of skipped, so re-authorize any in flight request. - Execution time policy governance verification is now on by default. It is relaxed only when
NODE_ENVistestordevelopment, andPOLICY_EXECUTION_VERIFICATION_ENFORCEDcan no longer switch it off in production. Every policy you execute against needs a signed approval record before you promote a deployment. ExecutionGatewayrequires apolicyRepositoryand apolicyApprovalVerifierat construction. Tests and examples that predate this can setallowUnverifiedPolicy: true, which production must not use.GatewayVerificationResultgainschecks.policyGovernanceVerifiedandpolicyGovernanceViolation.
- Signing a full Execution Trust Record under AWS KMS failed with
Member must have length less than or equal to 4096, because KMS refuses a raw Ed25519 message over 4096 bytes. A message over that size is now signed as a fixed 97 byte commitment (a prefix, a NUL byte, and the SHA-512 digest), and verifiers accept it. Signatures issued before this change are unaffected. A verifier you wrote yourself must implement the same rule to verify large KMS signed records, see the large message rule. The Python SDK’s offline verifier accepts the commitment form. That fix ships in Python SDK 1.1.5, published to PyPI on 2026-09-20. - The Python SDK now declares the
cryptographypackage thatparmana.crypto(the offline Trust Record verifier) imports. It is an optional extra,pip install "parmana[verify]", and is included in thedevextra. It was previously imported but undeclared, which made thePython SDKCI check fail onmypy. - The runtime now proves the evidence signing path works before it releases an action. If signing
is unhealthy the request is refused with
503 SIGNING_UNAVAILABLEand nothing is executed. If the action was released and the signed record then cannot be produced or persisted, the response is500 EXECUTION_RECORD_INCOMPLETEwith thebusinessTransactionIdandauthorizationId, and you must reconcile instead of retrying as a new transaction. Enforced everywhere exceptNODE_ENVtestanddevelopment. The startup log gainssigningReadinessConfigured. This mitigates the ordering gap G-52 and does not close it. - New page AWS KMS signing: create the key, the Vercel role and policy, the environment, and how to verify. Adds
npm run verify:kms(scripts/verify-kms-signing.ts), which signs test data through the repository’s ownKmsSigneron both sides of the 4096 byte KMS limit and requires KMS to refuse a raw message over it. Verified on 2026-09-20 against the real key with a limited IAM user, and end to end with the server signing a full Trust Record under KMS. Pages that said KMS was roadmap or not built (Cryptography, FAQ, Local deployment, Deploy patterns) are corrected. Azure Key Vault, Google Cloud KMS and HSM remain unimplemented. - An approved action whose connector is not configured now returns
503with codeCONNECTOR_NOT_REGISTEREDand the messageNo connector is registered for capability '<action>' on this deployment. Nothing was executed.Before, it was a bare500with no code, and only the server log named the cause. Both SDKs already expose the code (serverCodein TypeScript,server_codein Python 1.1.6). - Deployment artifacts found by following the production runbook on a fresh clone. The
Dockerfilecopied a package that no longer exists (packages/receipt) and skipped real workspaces, and the image did not containdocs/site/parmana-handbook.pdf, which the API needs at startup, so a container built from it could not have started.scripts/apply-all-migrations.sqlwas missing 7 of 30 migrations (includingexecution_audit_eventsandpolicies) and carried outdated text for others. It is now generated fromsupabase/migrations/withnpm run generate:migration-bundle, and a test fails when it drifts. The runbook and the policy governance guide now say that proposing a policy change also needs aUSER(human) key, because a service key is refused with403 NON_HUMAN_CALLER_DENIED. - TypeScript SDK 1.1.6 and Python SDK 1.1.6 (both published on 2026-09-20, so both SDKs now share one version). TypeScript: the client builds a default
HttpTransportfrom its own configuration whentransportis omitted, sotimeout,retryPolicyandapiKeyapply with no extra wiring (1.1.5 and earlier threwTransport is required.), anduserAgentis now sent as theUser-Agentheader. Python:InternalServerErrornow carries the server’s code onserver_code, so you can tellSIGNING_UNAVAILABLEfromEXECUTION_RECORD_INCOMPLETE. The Python client docstring now lists 429 among retried statuses. - TypeScript SDK 1.1.3 (published to npm on 2026-09-20):
InternalServerErrornow carries the Runtime’s owncodeonserverCode. Use it to tellSIGNING_UNAVAILABLE(503, nothing was executed, retry later with a newbusinessTransactionId) fromEXECUTION_RECORD_INCOMPLETE(500, the action was released, do not retry as a new transaction). The SDK never auto retriesPOST /execute, so nothing is retried for you.
2026-09-14
AddedcreateBusinessTransaction()(TypeScript) andcreate_business_transaction()(Python): derives the three id pairs a Business Transaction must keep consistent, removing the most common integration mistake. Both SDKs published:@parmana/sdk@1.1.2on npm,parmana@1.1.4on PyPI.- Documentation-site domain fixed to
docs.parmanasystems.com; repository links updated togithub.com/pavancharak/parmana.
2026-09-11
Added- Standalone offline signature verifiers with no network/disk dependency, TypeScript
(
OfflineVerifier) and Python (offline_verifier.py), cross-verified against each other. - Public-key discovery:
GET /keys/:keyIdandGET /.well-known/jwks.json. - Verification-key rotation support (
PARMANA_VERIFICATION_KEY_ID, a rotation script), so a key can be rotated without breaking verification of records signed under the old key. - Optional
HYBRID_SIGNATURE_REQUIREDpolicy flag: an opted-in deployment now detects a stripped post-quantum signature on a record that should have one.
- A production deployment with real caller authentication surfaced a database constraint gap:
successful authenticated requests could fail closed with a
503before policy ever ran. Fixed via a migration. FileKeyProviderreported the wrong signature algorithm for any key other than the default.
2026-09-10
AddedPostgresRateLimitStore:/executerate limiting is now durable and fleet-wide whenDATABASE_URLis configured (previously in-process only).GET /trust-records: bulk, paginated export of full signed Execution Trust Records.- Structured, level-gated logging (
createLogger/getLogger). npm run loadtest: a load-testing script forPOST /execute.
- A misconfigured KMS/HSM key provider (
KEY_PROVIDER=aws-kmsetc.) now fails closed at startup instead of silently falling back to storing keys on disk. GET /readynow reportsauthDisabledexplicitly in its JSON response.- The Gateway’s identity is now configurable (
PARMANA_GATEWAY_ID) instead of hardcoded. - A long-lived HubSpot credential with no rotation in 90 days now triggers a startup warning.
2026-09-09
AddedPolicyValidator.findRuleConflicts(): flags policy rules whose conditions can both be true at once, advisory only, verified against every shipped policy with zero false positives.
- Policy validation now fails closed on any rule-referenced fact with no
boundSignalsbinding, instead of a load-time warning nobody would see. All shipped policies updated. TenantKeyResolver: Execution Authorizations now sign under a per-tenant key when one is provisioned, instead of always the shared default key.
PRIMARY_SIGNATURE_PROVIDER/SECONDARY_SIGNATURE_PROVIDERacceptml-dsa-65as an alias for the internaldilithium3identifier.
2026-09-06 to 2026-09-07
Added- Execution-time Policy Governance verification: a tampered or unapproved policy can now be refused before evaluation runs (feature-flagged, off by default).
PolicyChangeApprovalRecordsignatures are now verified (not just content hashes), hash-chained, and re-checked periodically, not just at startup.- Per-caller tamper-evident audit-event chaining, with a standalone chain verifier.
- Signed caller-capability claims in the Execution Authorization payload (defense-in-depth).
2026-08-16 to 2026-08-19
Added- GitHub connector, with ephemeral GitHub-App-installation credentials.
- Stale documentation and code-comment references to the removed Razorpay connector cleaned up across the repository.
2026-08-18
Added- Policy Governance: a maker-checker workflow for policy changes (propose, approve, reject, signed approval records), plus a read-only internal review UI.
2026-08-12
Added- Caller-to-capability scoping (
allowedCapabilities).
- The Razorpay connector was removed from the repository entirely. It is not a supported integration path; see Integrations for what’s currently live.
2026-08-11
Added- Rate limiting on
POST /execute(per authenticated caller) andGET /health/GET /ready(per IP, more permissive). - TypeScript SDK renamed
@parmana/legacy-referenceto@parmana/sdk, plus retry logic (backoff on idempotent GETs against 502/503/504).
- Both SDKs’ documented quickstart examples are now run against a real server on every test run, not just syntax-checked. Found and fixed several real bugs in the process, including every TypeScript example importing a package name that never existed.
2026-08-09
Added- Hybrid signature support completed (classical + post-quantum), see Security.
2026-08-05
- HubSpot Signed Approval Artifact support, closing a policy-verification gap for that connector.
2026-07-28
Security- Fixed an execution-authorization bypass: policy signals are now bound to the executed
intent (
Policy.boundSignals+SignalIntentBinder), closing a gap where a caller could declare one action’s signals while a different action actually executed. Found via an external adversarial security exercise. See Security.
2026-07-18
Added- Caller bearer-key authentication, gating every route except
/health,/ready,/openapi.yaml, and/documentation. Any documentation describing an unauthenticated API predates this and is stale.
2026-07-14
Added- OpenAPI specification published, execution connector framework introduced.
2026-07-11 to 2026-07-13
Added- Credential isolation and secure enterprise-connector handling, see Credential isolation and Content binding & TOCTOU.
- Execution Trust pipeline and hybrid signatures introduced.
Earlier
- 2026-07-01: Python SDK
v1.0.0released on PyPI, the first versioned artifact. - 2026-06-25: First commit.