# Kindgi 0.1.9

> What's new, fixed and changed in Kindgi 0.1.9, its known issues, and the steps to upgrade to it from 0.1.8.

Released October 20, 2026. For the docs of this version, see [docs.kindgi.com/v0.1.9](https://docs.kindgi.com/v0.1.9/index.md).

## 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`, and `check stored-secrets` reports the version `aad-mismatch`. Normal reads get the same values.

- **An unhandled error's `500` no longer repeats the error's own text.** Such an error answered `500 internal-server-error` with 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 `500` no longer repeats an error's text.** A deployment write that failed, or a signed export whose signing failed, answered `500` with 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, from `KINDGI_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`, or `503 database-busy`, instead of an empty list or `404`, `check provider-keys` exits `2` and says why, where it found nothing, and `tenants repair` names 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 `GET` answers `503 database-busy` when the database is busy (`500` otherwise), a write `500`, 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-unreadable` in a decision's `resume`), where it failed.

- **The CLI sends a stored API key only to its own address.** A command could send the key that `kindgi auth login` or `kindgi dev` stored 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 `--token` or `KINDGI_API_TOKEN` goes where the command says, as before, and `kindgi doctor` has an `api-key` check. If you reach your server by another name, see [Upgrade steps](https://docs.kindgi.com/v0.1.9/releases/0.1.9.md#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 example `invalid 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](https://docs.kindgi.com/v0.1.9/deploy/people-and-keys.md#remove-a-person)).

- **Adding a turn to a conversation takes its opener, or `write` on its project.** Without a participant, only whoever opened a conversation, or someone with `write` on its project, can add a turn to it. Reading the conversation isn't enough. With a participant, the participant rule decides, as before ([Conversations](https://docs.kindgi.com/v0.1.9/guides/agents/conversations.md)).

  - A turn that names a project other than the conversation's is refused with `409 conversation-project-mismatch`. Name the conversation's project, or leave `projectId` out.
  - A turn into a conversation that doesn't exist answers `404 conversation-not-found` before it starts, with `wait: false` too, where it answered `202`.
  - Opening a conversation in a project you name (`projectId`) takes `write` there, as a run naming one outside its agent's projects does. For an agent more than one project serves, naming one of them takes `call` there.
  - Someone who can't read the conversation's project gets `404 conversation-not-found` instead of the `409` or the `403`.
  - An app that adds turns to conversations it didn't open, with a key that only runs the agent, now needs `write` on the conversation's project.

- **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 `read` on 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/deployments` lists only the deployments in projects you read, so a page can hold fewer than `limit` with `hasMore` true: page by `nextCursor`, not by count. `?projectId=` for a project you don't read, or one that isn't there, answers `403`, a tenant admin's too. Reading such a deployment, its `pack-env` or its `values`, or syncing its secrets, answers `404 deployment-not-found`. A deployment's secrets sync asks `admin` on its project, else the tenant, so a project admin's `kindgi deploy --project <id> --sync-secrets` now completes ([Who can do what](https://docs.kindgi.com/v0.1.9/deploy/people-and-keys.md#who-can-do-what)).

## New

- **Approvals by someone other than whoever asked:** a tool call's approval names who asked (`requestedBy`), and `conversationPolicy.hitl.separateApprover` makes 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; the `woocommerce` template turns it on for large refunds, price changes and publishing ([Someone other than whoever asked](https://docs.kindgi.com/v0.1.9/guides/approvals/ask-before-a-tool-runs.md#someone-other-than-whoever-asked)).

- **Who opened a conversation, who decided an approval:** a conversation records who opened it (`openedBy`), and `kindgi conversations list --opened-by=me` lists your own; one opened before 0.1.9 records no opener. `kindgi approvals list --decided-by=me` lists 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`, as `kindgi.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](https://docs.kindgi.com/v0.1.9/guides/tools/mcp-servers.md#make-a-write-happen-once)).

- **One flow's cost:** `GET /v1/cost/records` and `GET /v1/cost/aggregate` take `flowId`: every record of the run trees whose root is a run of that flow, whatever its version. With `groupBy=day`, that's the flow's spend by day. In the TypeScript client, `cost.usage.query` and `cost.usage.summary` take `flowId`; 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](https://docs.kindgi.com/v0.1.9/guides/observability/cost-per-run.md)).

- **The tenant's environments:** `GET /v1/environments` lists, 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's `client.environments.list()`. Every built-in store lists its names; a store that can't answers `501 environments-unsupported`, naming the store ([Store a secret](https://docs.kindgi.com/v0.1.9/guides/secrets/store-a-secret.md)).

- **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, or `PUT /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; `--check` stops before anything moves. `--make-live` also 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 `--pack` one 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 env` keeps its name for value sets ([`kindgi environments`](https://docs.kindgi.com/v0.1.9/reference/cli/environments.md) and [`kindgi promote`](https://docs.kindgi.com/v0.1.9/reference/cli/promote.md)).

- **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_MS` and `KINDGI_DB_IDLE_IN_TRANSACTION_TIMEOUT_MS` change them, in milliseconds; `0` turns 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`; `0` waits 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 a `db` warning naming the setting, its value and its variable. Migrations and the `kindgi-runtime` admin commands run with no statement timeout, and wait for a connection as long as it takes ([Self-host](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/self-host.md)).

- **`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-signed` leaves them out). Its `manifest.json` gives 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 `--resume` finishes an export in the same folder ([`kindgi exports`](https://docs.kindgi.com/v0.1.9/reference/cli/exports.md)).

- **A tenant whose admins can't sign in any more gets one back:** `kindgi-runtime tenants make-admin` makes 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-run` says what it would change, and the admin it gives is in the tenant's audit log, by `system`, with the `--reason` given ([Serve many tenants](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/serve-many-tenants.md)).

- **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 bot` is `support-bot`) unless `--slug` gives 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 what `kindgi deploy --project` takes ([Organize by org and project](https://docs.kindgi.com/v0.1.9/guides/projects/organize-by-org-and-project.md)).

- **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 add` says whether it went out (`--no-invite` invites them without the email). Once it expires, every way in refuses them until a tenant admin sends it again with `kindgi people invite`. `kindgi people list --pending` lists 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](https://docs.kindgi.com/v0.1.9/deploy/people-and-keys.md#their-invitation)).

- **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 a `standard` or `senior` reviewer, 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=true` turns it on, with authorization enforced and the runtime's public URL set ([Let a store's staff ask and approve in WordPress](https://docs.kindgi.com/v0.1.9/sites/wordpress.md)).

- **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_ADDRESSES` is where the server's own calls come from, such as its calls to MCP servers and its webhook deliveries; `KINDGI_EGRESS_DEPLOYMENT_ADDRESSES` is where calls made by the deployed code come from. `kindgi egress` prints them, anyone who belongs to the tenant reads them in `GET /v1/tenant`'s `egress` (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](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/egress-addresses.md)).

- **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 with `needs-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 without `firstAdmin`, is a service account's, so its runs count as unattended. Each run now says how it started, as `startedBy` on `GET /v1/runs` and `GET /v1/runs/{runId}`, and the `woocommerce` template declares it on its refund, price and publish tools ([When no person started the run](https://docs.kindgi.com/v0.1.9/guides/approvals/ask-before-a-tool-runs.md#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>` on `kindgi schedules create` or `kindgi webhooks register`, or `runAsServiceAccountId` from your app, and its runs act as the account, with the account's grants, and keep running after you leave. `take-ownership --run-as` hands an existing one to an account. Naming an account needs `act_as` on it, which tenant admins hold, and each change of owner is recorded as `trigger-owner-set` ([Run as a service account](https://docs.kindgi.com/v0.1.9/guides/runs/run-on-a-schedule.md#run-as-a-service-account)).

- **A runtime can limit how long it keeps finished runs:** `KINDGI_RUN_RETENTION_MAX_DAYS`, in whole days. A `run` policy that keeps runs longer, or for good, is refused with `400 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](https://docs.kindgi.com/v0.1.9/deploy/retention.md)).

- **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-factor` removes a person's when their phone is lost. Set `KINDGI_AUTH_PREVIOUS_SECRET_PATH` when you rotate the sign-in secret, so the keys move to the new one ([A second factor](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/sign-in.md#a-second-factor)).

- **Run outcomes and guardrail violations in the audit trail:** each run's end leaves a `run-outcome` audit 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 a `guardrail-violation` record. With `KINDGI_COMPLIANCE_CLASSIFIER=shipped`, both are kept 730 days ([Audit events](https://docs.kindgi.com/v0.1.9/deploy/retention.md#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-env` shows where each declared name is set, never a value (the TypeScript client's `deployments.packEnv`, the Python client's `deployments.pack_env`). `kindgi deploy --hosted` refuses an `env` in the pack's config for the env it deploys to, naming the command that sets each name. `kindgi env set … --force` sets 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](https://docs.kindgi.com/v0.1.9/guides/secrets/pack-env.md#in-a-deployment)). To run the pack services yourself, a system key with `system:pack-env` reads each deployment's values and records what its service runs ([A pack service for each deployment](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/serve-many-tenants.md#a-pack-service-for-each-deployment)).

- **`kindgi deployments get` and `list`:** `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. `list` lists the deployments, the latest activated first, and `--table` shows each one's pack service (`ready`, `ready, held: <code>`, `pending: <code>`, …). A ready endpoint carries `packEnvProblem` (`{ code, since }`) while its service runs on its last values ([Declare the environment your code reads](https://docs.kindgi.com/v0.1.9/guides/secrets/pack-env.md#in-a-deployment)).

- **Terms and privacy links on the sign-in page:** set `KINDGI_AUTH_TERMS_URL` and `KINDGI_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-options` answers them in `legal` (`termsUrl`, `privacyUrl`), and `createApp` takes them as `signInLegal` ([Terms and privacy links](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/sign-in.md#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](https://docs.kindgi.com/v0.1.9/guides/runs/run-on-a-schedule.md#in-the-console)).

- **A deployment-wide cap on invitation emails:** `KINDGI_AUTH_INVITE_EMAIL_DAILY_CAP` is 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.reason` is `capped`), and sending it again answers `429 invite-capped`. Set without `KINDGI_AUTH_EMAIL_SMTP_URL` or `KINDGI_AUTH_EMAIL_SMTP_URL_PATH`, the runtime doesn't start ([Their invitation](https://docs.kindgi.com/v0.1.9/deploy/people-and-keys.md#their-invitation)).

## Fixed

- **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 starting `database-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](https://docs.kindgi.com/v0.1.9/guides/runs/run-on-a-schedule.md#each-time-it-fires)).

- **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: `a` with tool `b.c` and `a.b` with tool `c` are both `a.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 purge` and `tenants repurge` delete 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 exit `2`: run it again ([Close and purge a tenant](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/serve-many-tenants.md#close-and-purge-a-tenant)). A `404` to the store's delete now stops the purge too, where it counted as deleted: OpenFGA answers `204` for a store already gone, so a `404` comes from something in front of it (check `KINDGI_OPENFGA_API_URL`).

- **`KINDGI_OPENFGA_API_URL` can end in `/`:** every OpenFGA call drops the trailing slashes. Before, the server's calls went to `//stores/…`, which OpenFGA answers `404`.

- **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 ends `cancelled`, with its child runs. Before, the refusal could end it `failed` first ([Suspend and restore](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/serve-many-tenants.md#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 `error` log 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 `schedules` warning: `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.get` and `tools.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 `…Unrecognized` member (such as `ScopeUnrecognized`), with every field kept. A stream event of a kind it doesn't know comes back as the typed event, not a `dict`. 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, with `assert_never`, now has the text or the `…Unrecognized` member to handle. The Python SDK's changelog has the details.

- **TypeScript client: a flow version's `unregisteredAt`:** `flows.versions.get` (and the deprecated `flows.getVersion`) is typed `FlowVersionRow`, so an unregistered version's `unregisteredAt` is in the type; the API already answered it. Code that reads it as a `Flow` is unaffected.

- **Reading a retired policy version:** `GET /v1/policies/{policyId}/versions/{version}` answers for a retired version too, with its `unregisteredAt`, as an agent's, a flow's and a gate policy's version routes do. Before, it answered `404 policy-not-found`. The TypeScript client's `policies.getVersion` and the Python client's `policies.versions.get` return a `PolicyVersionRow`. A version the retention sweep has purged still answers `404`. In the console, a retired version's page shows the version, marked retired since when, where it said `Version not found`.

- **A deploy with a schema that doesn't compile:** `POST /v1/deployments` compiles each agent's `output.schema`, and each flow's loop, fanout branch and subgraph `outputSchema` and its `output.schema`, as a turn or a run does. One that doesn't compile is refused with `400 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's `output` that isn't an object is refused at `/output`, by a deploy and by `POST /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 repair` names it (`couldn't be repaired: …`).

- **`kindgi deploy` lists a refused deploy's issues:** for `deployment-validation-failed` it read the issues from the wrong place, so their list never showed. It shows each one now.

- **`kindgi build` checks `--out` before 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 `--out` must 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 `--out` may be outside the pack. The build `kindgi deploy` runs 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-keys` log says so once. The add still answers `500`.

- **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 repair` puts 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/tenants` now 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](https://docs.kindgi.com/v0.1.9/deploy/people-and-keys.md)).

- **A typed agent offered no tools isn't told to call them:** when a turn offers the model no tools (no `tools`, and no `remember` from 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](https://docs.kindgi.com/v0.1.9/guides/agents/typed-answer.md)).

- **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 dev` is up, its demo agent and tool answer.** A read of the demo agent or tool that `kindgi dev` adds, in the moment after it said it was up, could be refused with `403`. 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 get `403 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-closing` and `409 tenant-suspended` messages, and the S3-compatible API's `403 AllAccessDisabled`, said nothing in it changes, though its deletes, revocations and erasures still go through. They now name those.

- **`kindgi exports create` counts revocations among what can still change a closing or suspended workspace's export.** The manifest's `consistency` line 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/deployments` names each deployment's project:** an entry's `projectId` is 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 `--hard` couldn'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 with `ifVersion` checks the version a reader sees: `0` after 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 `pending` with 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](https://docs.kindgi.com/v0.1.9/guides/webhooks/test-and-replay.md#fix-and-send-again)).

- **A deploy refused for a guardrail it can't keep says which:** `409 guardrail-project-mismatch` and `409 guardrail-already-registered` from `POST /v1/deployments` now carry `details.primitive` and `details.id`, as the deploy's other refusals do. They came without them.

- **`kindgi dev` on 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 dev` no longer fails a Scala pack's rebuild now and then with "sbt printed no classpath":** when sbt succeeds without printing the classpath, `kindgi dev` runs `export Runtime/fullClasspath` again, 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

- **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-routing` or `tool-errors` policy fails with `tenant-policy-unavailable` (503),** as one that can't apply its `hitl` policy already did. `details.policyKind` names the policy's kind. Such a turn used to end as `model-invocation-failed` (422), which reads as the caller's error.

- **`kindgi deploy --sync-secrets` on a hosted deploy needs `--env`** (`--hosted`, or an env block with `build: 'hosted'`). A hosted runtime reads a deployment's secrets in the env it serves. Without `--env`, the values in `.env.staging` went up under `staging`, 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.models` change, with no aliases. The scope of a secret or an env var is `ScopeTenant`, `ScopeOrg` or `ScopeProject` (was `Scope2`, `Scope3`, `Scope4`). `Flow` and `Tool` now name a flow and a tool (were `Flow1` and `Tool1`); what they named before is `JudgedRunContextFlow` and `RunWaitingApprovalTool`. The Python SDK's changelog lists every name.

- **`users.create` in 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 answers `UserCreated`, which isn't a `UserRecord`, so a type annotation or `isinstance` check on it changes.

- **Custom project and org bindings (breaking):** `ProjectBinding.delete` and `OrgBinding.delete` now 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. `ProjectDeleteOutcome` is `ok` or `project-in-environment-order`, and `OrgDeleteOutcome` is `ok`, `slug-conflict` or `org-has-environment-order`; `OrgDeleteConflict` is gone. `ProjectUpdateOutcome` gains `project-in-environment-order`, for a patch that changes the project's org. The conformance suites check the `ok` outcome.

- **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 ends `cancelled`, with `project-suspended`, `tenant-suspended` or `tenant-closing` in its new `cancelReason` ([`project-suspended`](https://docs.kindgi.com/v0.1.9/reference/errors.md#project-suspended)). `kindgi proposals passes list` shows a cancelled pass's reason.

- **A cleared deployment says whether its pack service comes back:** a `cleared` endpoint now says `waiting: true` when a pack service is wanted for it again, as after its tenant, project or pack is restored, and its `problem` says why it has none yet, as a `pending` one's does. Otherwise `retired` says why none is wanted: `superseded` is for good, and `tenant-suspended`, `project-suspended`, `pack-suspended` and `tenant-closing` last until the restore. `kindgi deploy --hosted` waits 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-unverifiable` because a registry read failed says why, in `details.cause`: `image-not-found`, `image-file-not-found`, `image-registry-unreachable` or `image-registry-unauthorized`. `kindgi deploy` gives a hint for each ([`image-unverifiable`](https://docs.kindgi.com/v0.1.9/reference/errors.md#image-unverifiable)).

- **MCP: an endpoint id has no `.`:** `POST /v1/mcp/endpoints` refuses one with `400 invalid-mcp-endpoint`, reason `dot-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](https://docs.kindgi.com/v0.1.9/guides/tools/mcp-servers.md)).

- **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 with `400 secret-name-invalid`, and the message says the rule. It's checked wherever a secret is first stored: `kindgi secrets set` and `POST /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](https://docs.kindgi.com/v0.1.9/guides/secrets/store-a-secret.md)).

- **Approvals: the tenant's admin reads every tier:** with authorization on, a person who is the tenant's admin reads approvals (`GET /v1/approvals` and one approval) in the projects they may read. With no roster entry, they read as an `admin` reviewer; 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, a `member` key 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](https://docs.kindgi.com/v0.1.9/guides/approvals/approvals-reference.md)).

- **A read the database is too busy for answers `503`:** a `GET` or `HEAD` that a lock or statement timeout ended, or that waited too long for a database connection, answers `503 database-busy` with `Retry-After: 5`, where it answered a `500`. Any other method keeps the `500`, since a write may have saved part of its work. The TypeScript client's server error carries `retryAfterSeconds` from a `503`'s `Retry-After`; it still doesn't retry on its own ([`database-busy`](https://docs.kindgi.com/v0.1.9/reference/errors.md#database-busy)).

- **An OpenFGA call gives up sooner:** the runtime retries a call that OpenFGA refuses (`429`, or a `5xx` but `501`) once, not up to three times, and waits at most a second, however long a `Retry-After` asks. 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 long `Retry-After`.

- **`kindgi-runtime check stored-secrets` only 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 a `KMS probe OK` line, and with no stored secrets it says so without contacting the KMS ([Back up and restore](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/back-up-and-restore.md#restore-into-a-fresh-database)).

- **An MCP endpoint's token errors say why:** when the deployment's `KINDGI_TENANT_HOST_ACCESS` refuses a token endpoint's address, `host-access-denied` now says what that level refuses, such as "the cloud metadata endpoints and the server's own host are refused" at `deployed`. A token endpoint's answer larger than 64 KiB, or one that stalls, isn't read: the call fails with `mcp-auth-failed`, which says so, whatever the answer's status (a 401 that large was `mcp-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](https://docs.kindgi.com/v0.1.9/concepts/security.md#tenants-and-isolation)).

- **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](https://docs.kindgi.com/v0.1.9/guides/webhooks/test-and-replay.md)).

- **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_WORKERS` sets how many workers (`0` renders in the server process, as before), and `KINDGI_TEMPLATE_RENDER_WORKER_HEAP_MIB` each one's heap cap (default 128). A runtime serving one tenant starts no worker unless you set `KINDGI_TEMPLATE_RENDER_WORKERS` ([Environment variables](https://docs.kindgi.com/v0.1.9/reference/env-vars.md#kindgi_template_render_workers)).

- **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 `\u003c` so 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](https://docs.kindgi.com/v0.1.9/guides/webhooks/start-runs-from-webhooks.md#what-its-agents-read)).

- **A scheduled retention sweep purges everything due, once per interval:** with `KINDGI_RETENTION_SWEEP_INTERVAL_MS` set, 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](https://docs.kindgi.com/v0.1.9/deploy/retention.md#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 one `boot` error record on stdout, whatever `KINDGI_LOG_LEVELS` says, and nothing on stderr. A crash while starting is a `boot` record with `err` and 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 OK` and `Secrets manager probe OK` are `boot` info records on stdout. Lines written before the logger exists, such as a `KINDGI_LOG_LEVEL` it can't use, stay on stderr. `kindgi dev` shows 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 `kindgi` CLI 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 exits `1` before it does anything ([What you need](https://docs.kindgi.com/v0.1.9/start/install.md#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](https://docs.kindgi.com/v0.1.9/guides/runs/run-on-a-schedule.md#run-as-a-service-account)).

- **The guardrail skills say up front what a guardrail can do:** only `halt` acts (`retry`, `escalate` and `compensate` are 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 the `on-violation` docs in each SDK say the same ([Guardrails](https://docs.kindgi.com/v0.1.9/guides/guardrails.md)).

- **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 `--hard` stops the model calls that read it (`secret-revoked`): they don't fall back to the org's or the tenant's key ([Models](https://docs.kindgi.com/v0.1.9/guides/models.md#keys)).

- **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 `--hard` now 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, by `kindgi tools publish` or 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 with `409 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 a `WARN`. Only active versions count ([When a tool id is taken](https://docs.kindgi.com/v0.1.9/guides/tools/mcp-servers.md#when-a-tool-id-is-taken)).

- **`--hosted` works on every command.** In a pack folder where `kindgi dev` ran, `.kindgirc.json` points the CLI at the dev runtime, and only `kindgi build` and `kindgi deploy` could skip it. Now a command given `--hosted`, or acting in an env with `build: 'hosted'`, goes to the CLI's own address (`--url`, `KINDGI_API_URL`, or the one `kindgi auth login` stored) 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 --hosted` is 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/deployments` with a `projectId` records that project as where it's read, not the broadest scope the deployer administers. So a tenant or org admin's first `kindgi deploy --project <projectId> --sync-secrets` reads 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](https://docs.kindgi.com/v0.1.9/guides/projects/organize-by-org-and-project.md#deploy-a-clients-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](https://docs.kindgi.com/v0.1.9/guides/sso/sign-in.md)).

- **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

- **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 is `prod` on Kindgi Cloud and your runtime's `KINDGI_ENV` on your own. Runs still read each environment's own key first ([`kindgi promote`](https://docs.kindgi.com/v0.1.9/reference/cli/promote.md)).

## 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.json` or `.kindgirc.json` with a `token` and no `apiUrl` beside 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](https://docs.kindgi.com/v0.1.9/deploy/self-hosted/self-host.md)).

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