Kindgi 0.1.9
Released October 20, 2026. For the docs of this version, see docs.kindgi.com/v0.1.9.
Security fixes
Section titled “Security fixes”-
A stored secret's value opens only for the tenant that reads it. A secret is looked up by the reading tenant, and row-level security checks the tenant again, but its value was bound to the tenant of the row found. So a read that ever found another tenant's row would have got a value that opens. The value is now bound to the reading tenant: such a read fails with
secret-decryption-failed, andcheck stored-secretsreports the versionaad-mismatch. Normal reads get the same values. -
An unhandled error's
500no longer repeats the error's own text. Such an error answered500 internal-server-errorwith its own message, which could name an internal address. Its body now carries a fixed message and the request id, and the server's log keeps the whole error. -
A deployment's or a signed export's
500no longer repeats an error's text. A deployment write that failed, or a signed export whose signing failed, answered500with the error's own message. Its body now carries a fixed message and the request id, and the server's log keeps the cause. The error codes are the same. -
A database connection's error no longer names an address. Such an error's text can reach a caller, in a
500's message or a schedule fire's note. It named the host or address the server tried, and a refusal of the connection's credentials named the database user and the server's own address. It now says what failed (connect ECONNREFUSED,getaddrinfo ENOTFOUND, or that the database refused the connection's credentials or host) without them. The server's own log still says where the database is, fromKINDGI_DATABASE_URL, without the credentials. -
A model provider's key stays with its provider when the providers can't be read. If reading a tenant's model providers failed, such as on a database error, the check that keeps a provider's key away from tools, MCP endpoints and webhook signing found no providers, so a tool or endpoint naming a provider's key could get it. Now, while they can't be read, nothing that names a secret gets one: a tool's call and an MCP endpoint's connection fail with the failed read, and a webhook delivery isn't signed or sent. A turn fails too, the providers API answers
500, or503 database-busy, instead of an empty list or404,check provider-keysexits2and says why, where it found nothing, andtenants repairnames the tenant, leaves it to you and removes nothing. -
A check that can't read what it checks now refuses. If a check's own read failed, such as on a database error, it took the thing as not there and let the request through: an eval run could be read, cancelled or rescored by someone its project would refuse; a reinstate, a deploy or a proposal's evaluation went on without its project check; and a conversation opened without a project was checked against the Default project. Such a read now fails, and so does the request: a
GETanswers503 database-busywhen the database is busy (500otherwise), a write500, and nothing is served or changed. A schedule, an agent's or a flow's version, or a list of versions, whose read fails answers the same, where it said there was none or an empty list. A run whose resume can't read its version is left to resume later (run-version-unreadablein a decision'sresume), where it failed. -
The CLI sends a stored API key only to its own address. A command could send the key that
kindgi auth loginorkindgi devstored to an address other than the one stored with it. Now every command sends it only there, and refuses before it sends anything elsewhere, naming both addresses. A key given with--tokenorKINDGI_API_TOKENgoes where the command says, as before, andkindgi doctorhas anapi-keycheck. If you reach your server by another name, see Upgrade steps. -
A failed database query's values stay out of the server's log. A failed query's SQL and the values bound to it, or a value the database rejected as invalid input, could reach the server's log, and such a value can be personal data, such as a person's email. The line now says
A database query failed., or what the database rejected and its SQLSTATE (for exampleinvalid input for its type (22P02)), and still shows where the query ran. The answer's status and code are the same. -
A service account keeps no grant its record doesn't show. A grant whose write failed after the authorization store had applied it, such as on a timeout, stayed in the store, so the service account had access its record didn't show. Such a grant is now taken back.
-
A request no longer leaves work or open files behind. A request could leave server work or open files behind after it ended, so a caller with read access could use up the server's resources. That work now stops when its request ends, and what a request opens is closed. A stream's reader that stays gets the same events.
-
Removing a person takes away everything they held. A removed person kept the access that came with what they had made, such as an agent, a flow, a tool or an org, and their schedules and webhook triggers could go on running as them. Removal now takes everything they held, and their schedules and webhook triggers stop running as them until someone takes them over. For someone removed on an earlier runtime, take over their schedules and webhook triggers, then remove them again (Remove a person).
-
Adding a turn to a conversation takes its opener, or
writeon its project. Without a participant, only whoever opened a conversation, or someone withwriteon its project, can add a turn to it. Reading the conversation isn't enough. With a participant, the participant rule decides, as before (Conversations).- A turn that names a project other than the conversation's is refused
with
409 conversation-project-mismatch. Name the conversation's project, or leaveprojectIdout. - A turn into a conversation that doesn't exist answers
404 conversation-not-foundbefore it starts, withwait: falsetoo, where it answered202. - Opening a conversation in a project you name (
projectId) takeswritethere, as a run naming one outside its agent's projects does. For an agent more than one project serves, naming one of them takescallthere. - Someone who can't read the conversation's project gets
404 conversation-not-foundinstead of the409or the403. - An app that adds turns to conversations it didn't open, with a key
that only runs the agent, now needs
writeon the conversation's project.
- A turn that names a project other than the conversation's is refused
with
-
A deployment is read only by those who read its project. With authorization on, a member of a tenant could read every deployment in it. Now a deployments route asks
readon the deployment's own project as well as on the tenant. A deployment that names no project is the Default project's, and a tenant admin still reads them all.GET /v1/deploymentslists only the deployments in projects you read, so a page can hold fewer thanlimitwithhasMoretrue: page bynextCursor, not by count.?projectId=for a project you don't read, or one that isn't there, answers403, a tenant admin's too. Reading such a deployment, itspack-envor itsvalues, or syncing its secrets, answers404 deployment-not-found. A deployment's secrets sync asksadminon its project, else the tenant, so a project admin'skindgi deploy --project <id> --sync-secretsnow completes (Who can do what).
-
Approvals by someone other than whoever asked: a tool call's approval names who asked (
requestedBy), andconversationPolicy.hitl.separateApprovermakes it need someone else: the person who asked isn't offered Approve, and the API refuses their approve (403 separate-approver-required). Off by default; thewoocommercetemplate turns it on for large refunds, price changes and publishing (Someone other than whoever asked). -
Who opened a conversation, who decided an approval: a conversation records who opened it (
openedBy), andkindgi conversations list --opened-by=melists your own; one opened before 0.1.9 records no opener.kindgi approvals list --decided-by=melists the approvals you decided. A decision keeps the reviewer's name (deciderName) and its session's provider (sessionProvider). -
An MCP tool call's idempotency key: a run's MCP tool calls send the call's idempotency key in the request's
_meta, askindgi.com/idempotency-key. It's the same when a flow step retries the call, so a server can answer a repeat once. A model's new call gets a new key (Make a write happen once). -
One flow's cost:
GET /v1/cost/recordsandGET /v1/cost/aggregatetakeflowId: every record of the run trees whose root is a run of that flow, whatever its version. WithgroupBy=day, that's the flow's spend by day. In the TypeScript client,cost.usage.queryandcost.usage.summarytakeflowId; in the Python client,flow_id. In the console, a flow's row on a project's Cost page filters the page to it, and a flow's page opens it filtered to that flow with Cost (Cost per run). -
The tenant's environments:
GET /v1/environmentslists, for a tenant admin, each env name that holds an env value or a secret, at any scope, once, in byte order; a revoked secret doesn't count. In the TypeScript and Python clients, it'sclient.environments.list(). Every built-in store lists its names; a store that can't answers501 environments-unsupported, naming the store (Store a secret). -
Environments, from staging to production: a tenant admin sets an environment order, the projects a build goes through on its way to live, in order, for the tenant or for one org:
kindgi environments set, naming the environments in the pack config, orPUT /v1/environment-orders/tenant(/orgs/{orgId}). A build enters at the order's first environment: a deploy into a later one is refused, and so is publishing a version there,409 environment-takes-promotions.kindgi promote --from=<env> --to=<env>moves the same signed build to the next environment, with the edits made since to its agents and flows. It promotes only once every value the build needs there is set, and prints each one that isn't, with the command that sets it;--checkstops before anything moves.--make-livealso makes each agent the build carries live there, each through its gate. Each project that serves an item governs it there: who reads it, changes it and runs it. A live change for the whole tenant or an org, for an agent with a project in an order, is refused,409 live-scope-spans-environments: make it live one project at a time.kindgi deployments list --project=<project>lists one environment's deployments, and--packone pack's. A project in an order can't be deleted, or moved into or out of an org, until it's taken out of the order, and an org that has an order can't be deleted until it's cleared (409 project-in-environment-order,409 org-has-environment-order).kindgi envkeeps its name for value sets (kindgi environmentsandkindgi promote). -
Database timeouts: the runtime's connections to Postgres carry their own timeouts: 60 s a statement, 30 s waiting for a lock and 30 s idle in a transaction.
KINDGI_DB_STATEMENT_TIMEOUT_MS,KINDGI_DB_LOCK_TIMEOUT_MSandKINDGI_DB_IDLE_IN_TRANSACTION_TIMEOUT_MSchange them, in milliseconds;0turns one off. A query waits at most 10 s for one of the server's pooled connections, then fails before it runs (KINDGI_DB_POOL_WAIT_TIMEOUT_MS;0waits as long as it takes). The startup line shows the ones in effect. A timeout that ends a request's statement or transaction, or a wait for a connection past the pool's, is logged as adbwarning naming the setting, its value and its variable. Migrations and thekindgi-runtimeadmin commands run with no statement timeout, and wait for a connection as long as it takes (Self-host). -
kindgi exports create --out <dir>exports a whole workspace as files through the API, as a tenant admin: one JSON Lines file per kind of record, the artifacts' bytes, and, when the server signs exports, each decided approval's audit bundle, each run's provenance and the compliance evidence (--no-signedleaves them out). Itsmanifest.jsongives each kind's outcome, and names what an export never holds: secret values, pack images, and pack and run logs. A kind that fails doesn't stop the others, and--resumefinishes an export in the same folder (kindgi exports). -
A tenant whose admins can't sign in any more gets one back:
kindgi-runtime tenants make-adminmakes one of the tenant's people, or a new person by email, its admin, with what a first admin holds. They sign in to use it, so it needs sign-in turned on.--dry-runsays what it would change, and the admin it gives is in the tenant's audit log, bysystem, with the--reasongiven (Serve many tenants). -
Make and rename a project from the CLI and the console:
kindgi projects create --name=<name>makes a project, in an org with--org=<org-id>, and prints it with its id. Its slug is the name as a slug (Support botissupport-bot) unless--sluggives one.kindgi projects rename <project-id>changes its name, its slug, or both. In the console, a tenant admin makes one with New project on the tenant dashboard's Projects card, and a project's admins rename it with Rename on its Dashboard. A new slug changes whatkindgi deploy --projecttakes (Organize by org and project). -
People added with an email are invited, where people sign in: the runtime emails them an invitation to sign in, open for 7 days, and
kindgi people addsays whether it went out (--no-inviteinvites them without the email). Once it expires, every way in refuses them until a tenant admin sends it again withkindgi people invite.kindgi people list --pendinglists who hasn't signed in yet. In the console, People marks who's invited and whose invitation expired, and a person's page sends it again (People and keys). -
A store's staff ask and approve in WordPress, each as themselves: a site can sign its own people in to one project. The Kindgi plugin for WordPress adds a Kindgi page to wp-admin, where the site's staff ask the project's agents and decide approvals, and a decision names the person, shown as "Jane, via Acme store". A tenant admin registers the site with
kindgi sites register, and the plugin connects with the one-time code it prints. The site's settings decide which WordPress roles may sign in and what each gets: asking the agents, deciding approvals as astandardorseniorreviewer, and reading the project, never more. An allowed role with no entry in the role map only reads the project. A site's people get 15-minute sessions held to its project, and no API keys. Site sign-in is off by default:KINDGI_SITE_SIGN_IN=trueturns it on, with authorization enforced and the runtime's public URL set (Let a store's staff ask and approve in WordPress). -
Publish the addresses your calls come from: a runtime can publish the IP addresses its calls to other sites come from, so a site that admits only known addresses can admit them.
KINDGI_EGRESS_SERVER_ADDRESSESis where the server's own calls come from, such as its calls to MCP servers and its webhook deliveries;KINDGI_EGRESS_DEPLOYMENT_ADDRESSESis where calls made by the deployed code come from.kindgi egressprints them, anyone who belongs to the tenant reads them inGET /v1/tenant'segress(in the TypeScript client,tenant.get()), and the console's workspace home page shows them. Publishing them changes nothing about where calls go (Publish the addresses your calls come from). -
A tool can ask for a person's yes whenever no person started the run: a tool that declares
needsApprovalWhenUnattended: true(in Python,needs_approval_when_unattended=True) waits for a reviewer on every call an agent makes in a run a schedule, a webhook, an event or a service account's key started, whatever the agent's own approval rules or your tenant's say. A flow's tool step that calls it fails withneeds-a-person, since a step can't wait: call it from an agent step. In a run a person started, signed in or with their own API key, the agent's gates decide as before. A tenant's first admin key, when the tenant was made withoutfirstAdmin, is a service account's, so its runs count as unattended. Each run now says how it started, asstartedByonGET /v1/runsandGET /v1/runs/{runId}, and thewoocommercetemplate declares it on its refund, price and publish tools (When no person started the run). -
A schedule or a webhook trigger can run as a service account: name one with
--run-as=<service-account-id>onkindgi schedules createorkindgi webhooks register, orrunAsServiceAccountIdfrom your app, and its runs act as the account, with the account's grants, and keep running after you leave.take-ownership --run-ashands an existing one to an account. Naming an account needsact_ason it, which tenant admins hold, and each change of owner is recorded astrigger-owner-set(Run as a service account). -
A runtime can limit how long it keeps finished runs:
KINDGI_RUN_RETENTION_MAX_DAYS, in whole days. Arunpolicy that keeps runs longer, or for good, is refused with400 retention-over-limit, and a*policy, a policy stored before the limit was set, or no policy at all purges them at the limit. Unset, there's no limit. The startup log gives the limit, and the console's Deleted data page says when the server's limit decides (Keep and purge deleted data). -
A second factor at sign-in: a person can add an authenticator app's codes and 10 single-use recovery codes, set up in the console. Signing in by the emailed link, Google, Microsoft or GitHub then asks for a code before any session is made. A tenant admin can require one for the workspace with
kindgi sign-in settings --require-second-factor=on: sign-in through the workspace's own identity provider and a site's people are exempt, console sign-in with an API token is refused, and API keys aren't covered.kindgi people reset-second-factorremoves a person's when their phone is lost. SetKINDGI_AUTH_PREVIOUS_SECRET_PATHwhen you rotate the sign-in secret, so the keys move to the new one (A second factor). -
Run outcomes and guardrail violations in the audit trail: each run's end leaves a
run-outcomeaudit record, written with the end itself, child runs, replays, eval runs and dry runs included. Each guardrail that blocks a step's answer, or lets it through with a violation, leaves aguardrail-violationrecord. WithKINDGI_COMPLIANCE_CLASSIFIER=shipped, both are kept 730 days (Audit events). -
A pack service for each deployment gets its pack's env: where a pack service is run for each deployment, as on Kindgi Cloud, it's given the values of the names its pack declares, plain or secret, as they're set for the env the server serves: at the deployment's project, then its org, then the tenant (
kindgi env set NAME <value> --scope=… --env=…,kindgi secrets set NAME --scope=… --env=…). The audit record has a row each time it's given a secret, never the value. While a required name is set nowhere (pack-env-missing), or a name is set both as a secret and as a plain value (pack-env-ambiguous), it isn't started, and one that's running keeps the values it has.GET /v1/deployments/{deploymentId}/pack-envshows where each declared name is set, never a value (the TypeScript client'sdeployments.packEnv, the Python client'sdeployments.pack_env).kindgi deploy --hostedrefuses anenvin the pack's config for the env it deploys to, naming the command that sets each name.kindgi env set … --forcesets a value that's already there again, so a pack service whose newest values didn't start tries them again (Declare the environment your code reads). To run the pack services yourself, a system key withsystem:pack-envreads each deployment's values and records what its service runs (A pack service for each deployment). -
kindgi deployments getandlist:get <deployment-id>shows a deployment (its project, its image, what it ships) and its pack service now: ready, pending or cleared, and, when it runs on the values it was last given, why by code, since when, and the command that sets what's in the way.listlists the deployments, the latest activated first, and--tableshows each one's pack service (ready,ready, held: <code>,pending: <code>, …). A ready endpoint carriespackEnvProblem({ code, since }) while its service runs on its last values (Declare the environment your code reads). -
Terms and privacy links on the sign-in page: set
KINDGI_AUTH_TERMS_URLandKINDGI_AUTH_PRIVACY_URL, and the console's sign-in page links Terms and Privacy, each only when it's set. Each is an absolute https URL with no username or password, or the runtime doesn't start.GET /v1/auth/sign-in-optionsanswers them inlegal(termsUrl,privacyUrl), andcreateApptakes them assignInLegal(Terms and privacy links). -
Change who a trigger runs as, in the console: on a schedule's or an incoming webhook's page, a tenant admin's Change owner… sets who its runs act as, under Runs as: You — with your access, or one of the tenant's service accounts. Others who may take it over still get Take over (Run on a schedule).
-
A deployment-wide cap on invitation emails:
KINDGI_AUTH_INVITE_EMAIL_DAILY_CAPis the most invitation emails the runtime sends in 24 hours, from all its tenants together: 500 by default, a whole number from 1 to 100000. Each tenant's own limit stays. At either limit the person is still added, without the email (inviteEmail.reasoniscapped), and sending it again answers429 invite-capped. Set withoutKINDGI_AUTH_EMAIL_SMTP_URLorKINDGI_AUTH_EMAIL_SMTP_URL_PATH, the runtime doesn't start (Their invitation).
-
A busy database no longer pauses a schedule: a fire the database was too busy to start (a lock or statement timeout, or no pooled connection in time) is
refused, its note startingdatabase-busy:. It doesn't count toward the 5 in a row that pause the schedule, and doesn't start the count over (Run on a schedule). -
MCP: one tool id, one endpoint: when an endpoint's tool would get the id another endpoint's tool has (an endpoint id with a
.can make one:awith toolb.canda.bwith toolcare botha.b.c), the catalog keeps it with the endpoint that registered it first and skips the other, with a warning naming both. Two syncs of the same tool, from replicas starting together, say, take turns. An endpoint in the runtime's config file with a.in its id still registers, and the start warns, naming its key. -
Purging a tenant with a key for OpenFGA:
kindgi-runtime tenants purgeandtenants repurgedelete the tenant's OpenFGA store with what the server's own calls carry: the key (KINDGI_OPENFGA_PRESHARED_KEY) and, behind Cloud Run IAM, the ID token (KINDGI_OPENFGA_AUTH). In 0.1.8 they sent neither, so the delete was refused and the purge stopped with exit2: run it again (Close and purge a tenant). A404to the store's delete now stops the purge too, where it counted as deleted: OpenFGA answers204for a store already gone, so a404comes from something in front of it (checkKINDGI_OPENFGA_API_URL). -
KINDGI_OPENFGA_API_URLcan end in/: every OpenFGA call drops the trailing slashes. Before, the server's calls went to//stores/…, which OpenFGA answers404. -
A run in flight when its tenant or project is frozen ends
cancelled: when a tenant is suspended or closes, or a project is suspended, a run whose tool call or guardrail check a pack refused for that reason endscancelled, with its child runs. Before, the refusal could end itfailedfirst (Suspend and restore). -
Permission changes ride out a short OpenFGA outage: a grant, a revoke, a new project or a new agent that OpenFGA can't take is retried after 5 s, 30 s, 2 min, 10 min and 1 h, and given up on, with an
errorlog line, only after about 1 h 13 min. Before, it was given up on within a couple of seconds. Changes to the same access still apply in the order they were made. -
Schedules and webhook triggers hold no database connection while a run starts: a fire's start takes a 60 s lease on its trigger, renewed while it starts, where it held a lock and a connection for the whole start. A second process starting the same schedule tries again a tick later, rather than waiting. A schedule whose start a crashed process held is free again within 60 s. A start that loses its lease logs a
scheduleswarning:schedule <id>: the start of fire <fireId> lost its lease, so another start of it may go alongside(webhook trigger <id>: …for a webhook trigger). -
Python client: reading a flow or a tool:
flows.get,flows.get_version,tools.getandtools.get_version, sync and async, answer the flow or the tool. Before, a real one failed validation. -
Python client: a value it doesn't know yet: when a newer runtime answers with an enum value or a kind of object that this client doesn't know, the call no longer fails with
invalid-response. An enum value it doesn't know comes back as its text, and an object of a kind it doesn't know as its union's…Unrecognizedmember (such asScopeUnrecognized), with every field kept. A stream event of a kind it doesn't know comes back as the typed event, not adict. The Java client already did this. Request bodies stay closed: such a value is refused before it's sent. Code that matches every known value, withassert_never, now has the text or the…Unrecognizedmember to handle. The Python SDK's changelog has the details. -
TypeScript client: a flow version's
unregisteredAt:flows.versions.get(and the deprecatedflows.getVersion) is typedFlowVersionRow, so an unregistered version'sunregisteredAtis in the type; the API already answered it. Code that reads it as aFlowis unaffected. -
Reading a retired policy version:
GET /v1/policies/{policyId}/versions/{version}answers for a retired version too, with itsunregisteredAt, as an agent's, a flow's and a gate policy's version routes do. Before, it answered404 policy-not-found. The TypeScript client'spolicies.getVersionand the Python client'spolicies.versions.getreturn aPolicyVersionRow. A version the retention sweep has purged still answers404. In the console, a retired version's page shows the version, marked retired since when, where it saidVersion not found. -
A deploy with a schema that doesn't compile:
POST /v1/deploymentscompiles each agent'soutput.schema, and each flow's loop, fanout branch and subgraphoutputSchemaand itsoutput.schema, as a turn or a run does. One that doesn't compile is refused with400 deployment-validation-failed, at its path (/output/schema,/nodes/<i>/branches/<k>/outputSchema, …). Before, it deployed, and every turn or run then failed. An agent'soutputthat isn't an object is refused at/output, by a deploy and byPOST /v1/agents(400 validation-failed). -
Many tenants: a tenant's setup and OpenFGA: the authorization outbox's drain and a tenant's setup no longer hold a database row lock or connection while they call OpenFGA. A setup whose authorization write fails, for any reason but the tuple already existing, now fails: the tenant stays pending, its create says why, and
kindgi-runtime tenants repairnames it (couldn't be repaired: …). -
kindgi deploylists a refused deploy's issues: fordeployment-validation-failedit read the issues from the wrong place, so their list never showed. It shows each one now. -
kindgi buildchecks--outbefore it starts: in any language, it refuses the pack root itself and any directory that contains the pack, where the build would write over the pack's own files or over a folder of the same name beside the pack. A TypeScript pack's--outmust also be inside the pack, following symlinks: outside it, the build used to fail partway, unable to find the pack's dependencies. A Python or JVM pack's--outmay be outside the pack. The buildkindgi deployruns when there is no envelope follows the same rules. -
Adding a person no longer holds a database transaction open while it writes their tenant membership to the authorization store. If that write fails, the person is taken back, and the
api-keyslog says so once. The add still answers500. -
On Cloud Run, a Google token the runtime asks for comes within 10 seconds or fails. Before, a metadata server that never answered left the call waiting for good.
-
kindgi-runtime tenants repairputs back people's and service accounts' access: a person's tenant membership and a service account's grants are written to the authorization store after their record is saved. If the runtime stopped between the two, or they were added while the runtime had no authorization store, they were refused, and nothing put their access back. The repair now writes what the record gives each active person and service account, and its line for each tenant says how many memberships and service account grants it put back. -
A tenant's first admin key is made with its setup, or not at all: without
firstAdmin,POST /v1/system/tenantsnow makes the tenant's first admin key in the same transaction that finishes its setup. A setup that fails leaves no key behind, and one that finishes always answers with its key. Before, a failure late in the setup could leave a live admin key that no answer carried. -
Two tenant admins can't leave a tenant with none: removing a person, and taking tenant admin from one, now count the tenant's admins and make the change as one step. Before, two admins removing each other, or taking admin from each other, at the same moment could both pass the check and leave the tenant with no admin. Now one of them is refused with
409 last-tenant-admin. A person whose removal stopped partway no longer counts as one of the tenant's admins (People and keys). -
A typed agent offered no tools isn't told to call them: when a turn offers the model no tools (no
tools, and norememberfrom the agent's memory), a typed agent's system message now ends "Give your final answer as that JSON alone." Before, it also said to call the tools it needs first (Give an agent a typed answer). -
Logs: an error a storage error caused says what failed: its log line now shows that storage error's code and message. Before, it read
caused by: [object Object]. -
A request whose database connection closes during a transaction sends nothing more: when the database closes the connection (a restart, a failover, its idle timeout), the request fails as before, and no statement it sends after that reaches the database, so none can land in another request's transaction. The same now holds for a connection held to build an index, whose later statements could leave a pooled connection with no lock timeout or end another request's transaction. A connection ended mid-statement or reset during a transaction no longer fails or holds up the next request.
-
A slow MCP catalog sync keeps its lock: a tool's MCP catalog sync holds a lock so that only one sync of that tool runs at a time, across replicas. A sync slower than the server's idle-in-transaction timeout (2 minutes by default) lost the lock part way, so another sync of the same tool could run alongside it. The lock now holds for up to 15 minutes. If it still ends first, the sync finishes and the server warns:
The MCP catalog sync's lock on a tool ended before its sync did: …. -
Right after
kindgi devis up, its demo agent and tool answer. A read of the demo agent or tool thatkindgi devadds, in the moment after it said it was up, could be refused with403. It now makes them readable before it serves. If that takes more than two seconds, it serves anyway and finishes in the background. -
A runtime serving many tenants shows none of their admins the license countdown. With
KINDGI_TENANCY=multi, a runtime that so far had only its first tenant showed that tenant's admins its license countdown (GET /v1/license). Every tenant's admins now get403 license-operator-only, however many tenants it has. A runtime set up to serve one tenant still shows that tenant's admins the countdown, while its database holds only that tenant. -
The console's filter bars are named for screen readers. The Cost page's filter chips and the Runs page's filter bar are each a group named Filters, where a screen reader found no name for them. They look the same.
-
A closing or suspended workspace's refused write says what still changes in it. Its
409 tenant-closingand409 tenant-suspendedmessages, and the S3-compatible API's403 AllAccessDisabled, said nothing in it changes, though its deletes, revocations and erasures still go through. They now name those. -
kindgi exports createcounts revocations among what can still change a closing or suspended workspace's export. The manifest'sconsistencyline named only deletes and erasures, but such a workspace can still revoke an API key or a signing key, and the export reads both. -
A console address with a slug that isn't your workspace's says so. When you're signed in to one workspace, an address with another slug showed your own workspace's pages under that slug. It now shows Not your workspace, with links to open your workspace or sign out. A slug that names no workspace gets the same page.
-
GET /v1/system/deploymentsnames each deployment's project: an entry'sprojectIdis the project the deployment named, else the tenant's Default. A deployment that named no project, such as one made before 0.1.8, sent none. It's absent only when the tenant has no Default project. -
A secret set again after it was revoked is read again: in the runtime's own secret store, a secret revoked and then set again stayed revoked (
secret-revoked), and one revoked with--hardcouldn't be set again until retention removed it. Setting it again now lifts the revoke: its old versions stay revoked, and its versions go on from the last. A set withifVersionchecks the version a reader sees:0after a hard revoke. -
A webhook delivery redelivered while it's being sent goes out after that send, never beside it: the send is recorded, and the delivery stays
pendingwith its attempts back to 0, even when that was its last attempt. If the worker sending it stopped, it goes out once the worker's claim on it ends. Several redeliveries during one send go out once (Fix and send again). -
A deploy refused for a guardrail it can't keep says which:
409 guardrail-project-mismatchand409 guardrail-already-registeredfromPOST /v1/deploymentsnow carrydetails.primitiveanddetails.id, as the deploy's other refusals do. They came without them. -
kindgi devon Linux keeps running when a folder in a Python, Java or Scala pack is removed: it stopped. The removal still counts as a change, and builds when it's in the pack's code. Any other error on the watch, such as the system's limit on file watches reached, builds, since it may have hidden a change. -
kindgi devno longer fails a Scala pack's rebuild now and then with "sbt printed no classpath": when sbt succeeds without printing the classpath,kindgi devrunsexport Runtime/fullClasspathagain, up to two more times, and gives the error only if none prints it. -
A pack in a pnpm workspace prunes its image without stopping the build: the image's prune to production dependencies after the install is a second, filtered install, and pnpm asks before it removes the modules folders for it, which a build has no terminal to answer. pnpm 10 and 11 stopped the build there, and pnpm 9 skipped the prune and left the dev dependencies in the image. The prune now goes ahead without asking, and the build scripts' output survives it.
Changed
Section titled “Changed”-
With a pack service for each deployment, a pack call stays in the run's project. It goes to the newest deployment in that project that ships what it names, and a run with no project uses the Default project. A deployment is superseded only by newer ones in its own project. So a guardrail's pack check deployed only into another project can't run any more: deploy the check's pack into the run's project.
-
A turn that can't apply its tenant's
model-routingortool-errorspolicy fails withtenant-policy-unavailable(503), as one that can't apply itshitlpolicy already did.details.policyKindnames the policy's kind. Such a turn used to end asmodel-invocation-failed(422), which reads as the caller's error. -
kindgi deploy --sync-secretson a hosted deploy needs--env(--hosted, or an env block withbuild: 'hosted'). A hosted runtime reads a deployment's secrets in the env it serves. Without--env, the values in.env.stagingwent up understaging, which a runtime serving another env never read. Now such a deploy is refused, naming--env, before anything is built or sent. -
Python client: model names (breaking): 37 names in
kindgi.client.modelschange, with no aliases. The scope of a secret or an env var isScopeTenant,ScopeOrgorScopeProject(wasScope2,Scope3,Scope4).FlowandToolnow name a flow and a tool (wereFlow1andTool1); what they named before isJudgedRunContextFlowandRunWaitingApprovalTool. The Python SDK's changelog lists every name. -
users.createin the clients (breaking): the TypeScript client's answers with the person it added, not their id:const { userId } = await client.users.create(spec). The Python client's answersUserCreated, which isn't aUserRecord, so a type annotation orisinstancecheck on it changes. -
Custom project and org bindings (breaking):
ProjectBinding.deleteandOrgBinding.deletenow answer an outcome, never nothing: a delete that succeeds is{ kind: 'ok' }. A host that implements either binding returns it where it returned nothing before.ProjectDeleteOutcomeisokorproject-in-environment-order, andOrgDeleteOutcomeisok,slug-conflictororg-has-environment-order;OrgDeleteConflictis gone.ProjectUpdateOutcomegainsproject-in-environment-order, for a patch that changes the project's org. The conformance suites check theokoutcome. -
Eval runs and improvement passes stop with their project: starting an eval run in a suspended project, or an improvement pass for one of its agents, is refused with
409 project-suspended, as a run is. One already going when its project or tenant is suspended, or its tenant closes, stops at its next case. It endscancelled, withproject-suspended,tenant-suspendedortenant-closingin its newcancelReason(project-suspended).kindgi proposals passes listshows a cancelled pass's reason. -
A cleared deployment says whether its pack service comes back: a
clearedendpoint now sayswaiting: truewhen a pack service is wanted for it again, as after its tenant, project or pack is restored, and itsproblemsays why it has none yet, as apendingone's does. Otherwiseretiredsays why none is wanted:supersededis for good, andtenant-suspended,project-suspended,pack-suspendedandtenant-closinglast until the restore.kindgi deploy --hostedwaits for a waiting one as for a pending one, and stops at once on a retired one, saying why. Before, it gave up on both. -
Deploy: why an image couldn't be verified: a deploy refused with
image-unverifiablebecause a registry read failed says why, indetails.cause:image-not-found,image-file-not-found,image-registry-unreachableorimage-registry-unauthorized.kindgi deploygives a hint for each (image-unverifiable). -
MCP: an endpoint id has no
.:POST /v1/mcp/endpointsrefuses one with400 invalid-mcp-endpoint, reasondot-in-endpoint-id. A tool's id is<endpointId>.<tool name>, and a tool name may have a., so two endpoints could make the same tool id. An endpoint registered before with a.keeps its id, and still reads and unregisters (MCP servers). -
A new secret's name has a rule: 1 to 255 ASCII letters, digits,
.,-and_, starting with a letter, digit or_, not ending with., and with no two.in a row. A name that doesn't fit is refused with400 secret-name-invalid, and the message says the rule. It's checked wherever a secret is first stored:kindgi secrets setandPOST /v1/secrets, a new secret in the tenant's config, a deploy's secrets and a webhook's generated secret. A secret already stored keeps its name, and still reads, rotates and takes a new version (Store a secret). -
Approvals: the tenant's admin reads every tier: with authorization on, a person who is the tenant's admin reads approvals (
GET /v1/approvalsand one approval) in the projects they may read. With no roster entry, they read as anadminreviewer; with a lower role on the roster, they read their own tier and any tier they ask for. Deciding and signing an audit bundle stay the roster's. An API key reads as its user does, within its own limits: a key with no user, amemberkey and a key limited to a project read nothing new. The access audit records the tenant-admin check (authz-decision) where it changes what the roster gives (Approvals reference). -
A read the database is too busy for answers
503: aGETorHEADthat a lock or statement timeout ended, or that waited too long for a database connection, answers503 database-busywithRetry-After: 5, where it answered a500. Any other method keeps the500, since a write may have saved part of its work. The TypeScript client's server error carriesretryAfterSecondsfrom a503'sRetry-After; it still doesn't retry on its own (database-busy). -
An OpenFGA call gives up sooner: the runtime retries a call that OpenFGA refuses (
429, or a5xxbut501) once, not up to three times, and waits at most a second, however long aRetry-Afterasks. A call to a slow or overloaded OpenFGA now fails in about 21 seconds, where it could take about 43, or up to about 90 minutes with a longRetry-After. -
kindgi-runtime check stored-secretsonly decrypts: it no longer runs the KMS probe a server runs as it starts, which also encrypts, so an identity that may only decrypt (unwrap) with the KMS key runs it. Its output no longer starts with aKMS probe OKline, and with no stored secrets it says so without contacting the KMS (Back up and restore). -
An MCP endpoint's token errors say why: when the deployment's
KINDGI_TENANT_HOST_ACCESSrefuses a token endpoint's address,host-access-deniednow says what that level refuses, such as "the cloud metadata endpoints and the server's own host are refused" atdeployed. A token endpoint's answer larger than 64 KiB, or one that stalls, isn't read: the call fails withmcp-auth-failed, which says so, whatever the answer's status (a 401 that large wasmcp-auth-refused). An SSO issuer's discovery document, or a pack service's answer, that's too large or stalls says so too, where the message was generic. -
A runtime serving many tenants looks for authorization changes with less work: it no longer opens a transaction for each tenant every half second to look for them. It looks only at the tenants with changes waiting, and at all of them about every 30 seconds. The list of tenants with changes waiting is the one table outside row-level security, and holds a tenant's id and a time, nothing else of the tenant's (Security).
-
Webhook deliveries take less work on a runtime serving many tenants: the delivery worker no longer opens a transaction in each tenant every second. It looks only at the tenants with deliveries waiting, and at all of them about every 30 seconds. A retry after a failed attempt is picked up by that 30-second look, so it can start up to 30 seconds after its
nextAttemptAt. The retry schedule is unchanged (Test and replay deliveries). -
A runtime serving many tenants renders instruction templates in a worker process: a turn's instruction template renders off the thread that runs the turn, in one worker process started at the first render and shared across turns. It holds about 67 MiB at rest, and up to its heap cap under load. A render past its time limit has its worker stopped wherever the render is, so one template can't hold up other turns, and fails with the same message as before; a new worker takes the next render.
KINDGI_TEMPLATE_RENDER_WORKERSsets how many workers (0renders in the server process, as before), andKINDGI_TEMPLATE_RENDER_WORKER_HEAP_MIBeach one's heap cap (default 128). A runtime serving one tenant starts no worker unless you setKINDGI_TEMPLATE_RENDER_WORKERS(Environment variables). -
In a run a webhook or an event started, an agent reads its message as data, not instructions: the message reaches the model inside an
<event>block, with every<written as\u003cso nothing in a delivery can close the block, and the system message tells the model never to follow instructions found there. A JSON message stays JSON. This changes what an existing webhook-started agent sees, a flow's agent steps included: put a step's task in the agent's instructions, not in its input. The message stays data in every later turn of the conversation, whoever starts it (one an earlier runtime kept reads as it came), and in a replay of the run for an eval, a flow's agent steps included. A conversation store of your own keeps the mark,fromEvent, and gives it back.{{ input.* }}in an agent's instructions isn't wrapped: keep a delivery's free text out of them (What its agents read). -
A scheduled retention sweep purges everything due, once per interval: with
KINDGI_RETENTION_SWEEP_INTERVAL_MSset, each round goes page by page until nothing due is left, where a pass used to purge up to 500 records per domain. One replica runs each round, a round missed while the runtime was down runs at start, and one that stops part way is carried on (Sweep on a schedule). -
Logs: the runtime's start-up lines go through its logger. Under
KINDGI_LOG_FORMAT=json, a start that stops (a setting refused, no license key, a secrets key refused, a database that doesn't answer) is onebooterror record on stdout, whateverKINDGI_LOG_LEVELSsays, and nothing on stderr. A crash while starting is abootrecord witherrand its stack, exit 1, and a crash from a failed query names the driver's error, never the query or its values. The pretty format prints the same plain line on stderr as before, and exit codes don't change. In both formats,KMS probe OKandSecrets manager probe OKarebootinfo records on stdout. Lines written before the logger exists, such as aKINDGI_LOG_LEVELit can't use, stay on stderr.kindgi devshows why a runtime stopped while starting as text, under JSON too. -
A database URL the runtime can't read stops it with exit 2, naming
KINDGI_DATABASE_URL, as any setting it can't use does. It used to exit 1. -
The
kindgiCLI stops at once on a Node older than 22.12: on any Node from 12 up to 22.11, it prints one line naming the Node it needs and how to switch to it, and exits1before it does anything (What you need). -
A schedule or webhook whose owner is a service account that's no longer registered stops running: each fire is
refused, with authorization on or off, where with authorization off it used to run. Take it over, or delete it (Run as a service account). -
The guardrail skills say up front what a guardrail can do: only
haltacts (retry,escalateandcompensateare recorded on the turn, and nothing runs it again), a check sees only the current turn, and what a check keeps in memory is lost when the pack's process is replaced. The guardrails guide and theon-violationdocs in each SDK say the same (Guardrails). -
The console formats written words: a run's answer, a conversation's messages, an approval's description, a judged item, a judgment's reason and eval answers show their Markdown formatted (headings, lists, links, code). Where formatting changes something, a Formatted / As written switch shows the words exactly as written. Text inside a record, such as a flow's output fields, is formatted with no switch of its own: the record's JSON view is the as-written form. An answer a guardrail withheld stays as written. In an approval's description, a link shows where it goes after its words, its host or a mail link's address, unless the words already are that address.
-
A model provider's key is read where the run is: the project's secret of that name, else its org's, else the tenant's. Before, only the tenant's was read. An improvement pass reads it in its own project. A key revoked without
--hardstops the model calls that read it (secret-revoked): they don't fall back to the org's or the tenant's key (Models). -
With your own secret manager (
KINDGI_SECRETS_BACKEND=secret-manager): a secret set for an org is now read when the project has none, before the tenant's, as it already was with secrets in the database. -
With your own secret manager: a secret revoked with
--hardnow counts as missing, as it already did with secrets in the database, so the org's or the tenant's value is read instead. -
A tool id is an MCP endpoint's tool or a tool published to the runtime, never both. An endpoint's tools are filed in your tenant's Default project as
<endpoint id>.<tool name>. A tool published under the same id, bykindgi tools publishor a pack's deploy, used to land beside the endpoint's versions, and the id was then served as neither kind. Now a publish or a reinstate of the other kind is refused with409 tool-kind-mismatch, and nothing is written. A deploy into the Default project of a pack with such a tool is refused the same way, and nothing is deployed. An endpoint that lists a tool whose id a published tool holds skips that tool and logs aWARN. Only active versions count (When a tool id is taken). -
--hostedworks on every command. In a pack folder wherekindgi devran,.kindgirc.jsonpoints the CLI at the dev runtime, and onlykindgi buildandkindgi deploycould skip it. Now a command given--hosted, or acting in an env withbuild: 'hosted', goes to the CLI's own address (--url,KINDGI_API_URL, or the onekindgi auth loginstored) and never reads that file. A command that goes to the dev runtime while another address is in reach says so first, and how to reach the other.kindgi dev --hostedis refused. -
A deploy into a project reads a tool's unset secret where its sync writes it. When a secret a tool names is set nowhere yet,
POST /v1/deploymentswith aprojectIdrecords that project as where it's read, not the broadest scope the deployer administers. So a tenant or org admin's firstkindgi deploy --project <projectId> --sync-secretsreads the values it just synced. A secret set tenant-wide afterwards isn't read by a tool version deployed that way: set it at the project (kindgi secrets set <name> --env=<env> --scope=project:<projectId>). A secret already set is still read where it's found first, and versions deployed before keep what they recorded (Deploy a client's pack into its project). -
SSO sign-in doesn't call the secret store while it holds the provider's row. A SAML or OIDC sign-in reads the provider's secret references just before, and under the lock uses those values, up to 5 minutes old. Past that, or with none read, it calls the store once and logs a warning,
sso-secret-resolved-under-lock, naming the tenant and the field. -
The console's sign-in page: the form sits beside Kindgi's band, with links to kindgi.com and the docs; on a narrow screen the band is a bar above it. The email field asks for Your email (Sign in).
-
A server's start opens far fewer database connections for its migrations: it runs every package's migrations under one hold of the migration lock, on one database client, so a boot opens 2 connections for them besides the server's pool, where 0.1.8 opened one per package, 35 in all. A boot takes fewer of the database's connections, and fewer of the outbound ports a NAT gives each instance, such as Cloud NAT's.
Known issues
Section titled “Known issues”- A promotion finds a model provider's key only when it's stored for the
whole tenant. Stored in the project of the environment you promote to,
where its runs read it, the key is listed under
Not ready in <environment>:as set nowhere a run looks, and the command printed beside it doesn't clear that. Until it's fixed, store the key under the same name for the whole tenant too, from outside a pack:kindgi secrets set <name> --env=<value set> --scope=tenant, where the value set isprodon Kindgi Cloud and your runtime'sKINDGI_ENVon your own. Runs still read each environment's own key first (kindgi promote).
Upgrade steps
Section titled “Upgrade steps”-
A tool id that already holds both kinds' active versions (an MCP endpoint's tool and a published one) is still served as neither. To serve it again, unregister every active version of one kind; until then, a new version of either kind is refused.
-
CLI, another name for your server: if you reach a server by another name than the one you signed in with (an internal hostname, an IP address or a proxy's path), run
kindgi auth login --url <that name> --token <the key>once, or pass--token. A hand-written~/.kindgi/config.jsonor.kindgirc.jsonwith atokenand noapiUrlbeside it needs"apiUrl": "<url>"added. -
MCP, an endpoint id with a
.: registering it again is refused. Register it under an id with-or_in place of the.. Its tools' ids change with it, so update an agent or a flow that names them. -
Behind PgBouncer: the runtime sends its database timeouts as startup parameters, which PgBouncer refuses on its default settings, so the runtime can't connect. Before upgrading, have PgBouncer 1.26.0 or later pass them on (
track_extra_parameters) (Self-host). -
A start can stop on a held lock: a migration waits for a lock 5 s at a time, 6 times, then the start fails naming the table and the session holding it (end it with
pg_terminate_backend, or let it finish). Before, it waited with no end. -
The first start can pause permission changes for a few seconds: it rewrites the table of permission changes queued for OpenFGA (
fga_tuple_outbox), which keeps every change ever made: about 6 s for a million rows. Meanwhile, a change of permissions waits, on a server of the earlier version still running too. With many rows (SELECT count(*) FROM fga_tuple_outbox), upgrade at a quiet time.