# Kindgi 0.3.0

> What's new, fixed and changed in Kindgi 0.3.0, and the steps to upgrade to it from 0.2.0.

Released October 30, 2026. For the docs of this version, see [docs.kindgi.com/v0.3.0](https://docs.kindgi.com/v0.3.0/).

## Security fixes

- **Revoking an API key now always ends the console sessions it opened:** a session someone opened by signing in with an API key was ended when the key was revoked, but if ending it failed, the session went on. Now every request on such a session checks its key. Once the key is revoked or gone, the next request is refused with `401 auth-revoked`, "The API key this session was opened with is revoked: sign in again". If the key can't be read, the request fails: it never counts as a live key. Revoking a key that's already revoked ends any session it left ([Sessions](https://docs.kindgi.com/v0.5.0/guides/sso/sessions.md#signing-out)).

- **A tenant id given in upper case is one tenant.** Before this release, `KINDGI_TENANT_ID` was kept as you gave it, while the database, a key and a console session hand the id back in lower case. Where the two met, a runtime whose id was set in upper case read its tenant as two. An identity provider registered through a key or the console wasn't offered for its domains, and GitHub and the emailed link weren't refused there, on a domain you verified too. When the deployment's API token changed, console sessions of the earlier token could stay signed in: with `KINDGI_SEED_USER_ID` keeping the user, they weren't ended, and during a rolling update a session opened through a replica still on the earlier token was accepted. A start stopped with providers registered in both spellings, or with `KINDGI_AUTH_VERIFIED_DOMAINS` naming the tenant by its id. A retention purge or an erasure of files kept with a run could stop, the deployment's API token couldn't read those files when a key wrote them, and a replay of the erasures didn't find the tenant. Each of these now compares the id in any case. An id in lower case, the usual form, was never affected.

- **A database error no longer puts a query's SQL or its values in a run's failure, a fire's detail or a log line.** A database failure inside a flow's step, a schedule's dispatch, an eval run, the authorization queue or a background job could put the query's SQL and the values bound to it in the run's failure message, the fire's detail, the stored error or the log line. It now says what failed and its SQLSTATE, such as `invalid input for its type (22P02)`. Any other error keeps its own text, and a network failure says what it means first, such as `connection refused (connect ECONNREFUSED …)`.

- **A read of the database that fails no longer weakens a check.** When such a read failed, a turn could run without a guardrail it couldn't read, a secret could be read from a wider scope than the one that named it, a webhook's new signing secret could be made over one that was there, and a delivery could be signed without the previous secret. Each of these now fails instead.

- **A pausing pack tool's errors no longer keep its link's token.** A failing pack tool's error was cleared of the call's secrets and connection tokens, but not of the link a pausing tool is given, so an error that repeated the link or its token kept it in the tool's result, the run's events and the server's log. The token in the link is now cleared from the error too, and the rest of the link, the server's address, stays ([Wait for another system](https://docs.kindgi.com/v0.5.0/guides/tools/wait-for-another-system.md)).

- **A file kept in the runtime never opens as a page of the runtime's own address.** A browser that opens an artifact's download saves it as a file, so nothing an uploader stored runs where the console is signed in. A stored type that isn't a well-formed media type is served as `application/octet-stream`; one with a line break in it made the download fail.

- **An upper-case seed user id matches its memberships and roles.** Before this release, `KINDGI_SEED_USER_ID` was the only user id that could be in upper case, while memberships and the reviewer roster hand ids back in lower case. Where the two met, that person could, as a reviewer, approve their own requests where a second person's approval is required. They couldn't see or decide an approval addressed to them, and as the only reviewer they couldn't approve an Improve run's suggested change they had asked for. Their grants showed no projects, teams or reviewer role. Where a user id kept as given meets one read back in lower case, the two now match whatever the case. Removing a person whose id is in upper case, while a person with the same id in lower case exists, leaves that person the project and team memberships and the reviewer roster entry. With an authorization store, it also gives them each membership's role where they hold none on that project or team. The removal's audit entry names each one (`grantsGivenToTwin`).

- **A stored file changed outside the runtime is no longer served as the one kept.** The runtime keeps each artifact's sha256 and size, and now checks the bytes against them as they're read. A download of a file that was changed or swapped in its store stops before the end, and the runtime logs an error, `Artifact <id>'s stored bytes aren't the ones put; the read was stopped`, with the file's place in the store and both hashes, never its content. Files nobody changed download as before.

## New

- **The console's project Dashboard shows the runtime's health as a button.** The **Kindgi Core** card's status is a button in its colour: **Running**, **Not answering**, **Can't reach its database** or **Unknown**. It opens what the status means and which runtime answers: its address, its version, whether it runs in development mode, and whether it serves one tenant or many. A line under it names the runtime and its version, such as `localhost:4000 · 0.3.0`. When the console is from another release, the line ends with the console's release, in the warning colour.

- **`kindgi agents promote --wait` waits for the reviewer.** When the release gate policy asks for an approval, it names the approval and the command that approves it, then waits until a reviewer decides: an hour, or the time `--wait=30m` gives. It exits 0 once the version is pinned, and 3 when the approval is rejected or expires, when it's approved but something its check measured changed first (promote it again), or when the wait runs out. Ctrl-C stops the wait, not the request. `kindgi promote --make-live --wait` waits the same way for the versions a promotion carries ([Release an agent version](https://docs.kindgi.com/v0.5.0/guides/improve/release-an-agent-version.md#pin-through-the-release-gate)).

- **Every pin that covers a project, in one read.** `GET /v1/projects/<project-id>/pinned-versions` lists every agent's pins that cover a project: the tenant's, the project's org's, the project's own and its segments'. It shows only the agents and versions you can read, so page on `nextCursor`, not on the count. In the TypeScript client it's `projects.pinnedVersions.list`, and in the Python client `projects.pinned_versions.list` ([Live versions reference](https://docs.kindgi.com/v0.5.0/guides/improve/pinned-versions-reference.md#every-pin-that-covers-a-project)).

- **Flows and tools have pinned versions, as agents have live versions.** A pin says which version is in use in a project, an org or the whole tenant when nothing names one, so a new version can be published and tried without going into use where a pin covers. A run that names no flow version takes the version pinned for its project, else the newest the project reaches, and records which (`flowVersionVia`, `flowPinnedScope`). An agent or flow version made in a project a tool's pin covers locks that tool at the pinned version. The clients pin, move, unpin and read a pin's history (`flows.pinned`, `tools.pinned`), and a pinned version can't be unregistered: it answers `409 flow-version-pinned` or `409 tool-version-pinned`, naming the pins. `kindgi promote --make-live` also pins each flow version it carries in the target project, and prints them under "Flows pinned in". `kindgi exports create` writes each flow's and tool's pins and their changes ([Pin a flow's version](https://docs.kindgi.com/v0.5.0/guides/flows/pin-a-flow-version.md), [A pin holds a tool at a version](https://docs.kindgi.com/v0.5.0/guides/agents/agent-and-tool-versions.md#a-pin-holds-a-tool-at-a-version)).

- **Each change to a trigger makes a version of it, and a run names the version it used.** Registering a schedule, webhook or mail trigger makes its version 1. A change that alters it makes the next, and so does a take-over to another owner; pausing, resuming or removing it makes none. A version never changes. A run uses the trigger's newest version when it starts and keeps it, so a change reaches the next run that starts, never one already going. `GET /v1/schedules/{triggerId}/versions` pages a schedule's versions, newest first, and `…/versions/{version}` reads one (`schedules.versions` and `getVersion` in the TypeScript client, `list_versions` and `get_version` in Python), and the same for `/v1/webhooks` and `/v1/mail-triggers`. A trigger, a fire and a run's `trigger` name their `version`. In the CLI, `kindgi schedules`, `kindgi webhooks` and `kindgi mail-triggers` take `versions <id>` and `get <id> <version>`, and `list --table` has a `VERSION` column. `kindgi exports create` writes each trigger's versions. A trigger from before 0.3.0 has its first version at its next change or its first fire. Its versions keep every input it has had, so a secret pasted into one stays in an earlier version after you edit it out: rotate it ([Run on a schedule](https://docs.kindgi.com/v0.5.0/guides/runs/run-on-a-schedule.md#its-versions)).

- **The Cloud Run module's `upgrade.sh` upgrades every network shape.** It took a deployment with `network_mode = "none"` only, and one on a private network was upgraded by hand. Now a deployment of the module on runtime 0.1.8 or later, in any `network_mode`, can run it, to 0.2.0 or later: use it as of the `@kindgi/sdk@0.3.0` tag ([Cloud Run](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/cloud-run.md#operate-it)). From 0.2.0, a deployment whose database has a private IP can't upgrade to 0.3.0 with it yet ([Known issues](https://docs.kindgi.com/v0.5.0/releases/0.3.0.md#known-issues)).

- **An agent's model settings say how much its model thinks.** A model-settings block takes `thinking`, beside `temperature` and `maxOutputTokens`: `lowest` (as little as the model allows), `minimal`, `low`, `medium`, `high`, `xhigh` or `max`, and an improvement pass can try the levels its schema marks tunable. A provider's model lists the levels it takes in its `thinking`'s `levels`, and a `default` for a call that asks none. A level a model doesn't take goes up to the next one it takes, or to its highest, and the call warns `thinking-level-adjusted`. Each built-in adapter sends the level in its vendor's own field, and each model call's cost record says in `thinking` the level it asked for, the one sent, and the fields as they went out. The CLI's presets list each thinking model's levels: a provider registered from an older preset has none until you register it again. A runtime before 0.3.0 takes none, so there `kindgi providers register --preset` registers the models without them, and says so ([Temperature and thinking](https://docs.kindgi.com/v0.5.0/guides/models.md#temperature-and-thinking)).

- **A guardrail can judge a tool call, its result or a model reply, not only the final answer.** Its `at` says where: `'before-tool'`, a call before it runs (after its access check, before any approval); `'after-tool'`, what a call returned, before the model sees it; or `'model-step'`, a reply that asks for tool calls, before any of them runs. There, `halt` fails the turn, and `retry` refuses the call, withholds its result or sends the reply back, counting attempts across the turn. A check names where it can run with `defineCheck({ points })`, `scope.tools` limits a guardrail at a tool point to the tools it names, and a call a `before-tool` guardrail judges waits for the calls before it. A check that can't run at its point fails as `check-point-unsupported`, or `check-timed-out` past its time limit. With `sees: 'conversation'`, a check also gets the conversation's earlier turns ([Where in the turn it runs](https://docs.kindgi.com/v0.5.0/guides/guardrails/configure-a-guardrail.md#where-in-the-turn-it-runs)). A guardrail a pack defines doesn't get all of this in this release ([Known issues](https://docs.kindgi.com/v0.5.0/releases/0.3.0.md#known-issues)).

- **`GET /v1/runtime` says which runtime answers:** its release in `version`, such as `0.3.0`, whether it serves one tenant or many in `tenancy` (`single` or `multi`), and in `development` whether it runs in development mode. Any of the tenant's tokens reads it, whatever its rights. It's `runtime.get()` in the TypeScript client and `runtime.get` in Python ([Restart and check health](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/restart-and-check-health.md#check-health-and-logs)).

- **A flow run has a provenance record, as an agent turn has.** Once the run finishes, its record holds each step that ran, why it ran, the runs it started, and what the run's output came from: ids, kinds, statuses, times and codes, never a value. A very large run's record is cut from the middle and says what it left out. `GET /v1/provenance/{runId}` gives each step's model usage in `stepUsage`, and so does the signed export. `GET /v1/provenance?flowId=`, `kindgi provenance list --flow=<flow-id>` and the clients list one flow's runs' records. A finished run whose record isn't written yet answers `404 provenance-not-found`, and says whether it's being written or failed ([A flow's answers](https://docs.kindgi.com/v0.5.0/guides/observability/trace-an-answer.md#a-flows-answers)). A flow run that ended before the upgrade has no record. A person's erasure also reaches the records of the flows their runs ran in, and marks them erased.

- **`GET /v1/identity/me/permissions` says whether the license is out of your reach:** `licenseOperatorOnly` is `true` when `GET /v1/license` refuses you whatever your role, as on a runtime serving many tenants. The console reads it, and no longer asks for the license only to be refused, which left a refusal in the tenant's access audit. The `license-operator-only` refusals now say why in plain words, without a setting's name ([People, API keys and service accounts](https://docs.kindgi.com/v0.5.0/deploy/people-and-keys.md#what-you-may-do)).

## Fixed

- **A pnpm workspace pack's image no longer carries its dev dependencies:** a pack with build steps (`prisma()`'s `prisma generate`) installs its dev dependencies too, and prunes them after the steps. In a pnpm workspace, the prune left their folders in `node_modules/.pnpm`, where the pack couldn't load them but the image still carried them. Now they're dropped, on pnpm 9 to 12. Rebuild the pack's image with this CLI to leave them out; nothing else in the image changes.

- **`upgrade.sh`'s way back names the CLI of the release you upgraded from:** when the new revision doesn't come up, the steps it prints to export the erasure ledger and replay it ran `npx @kindgi/cli@0.1`, whatever the release. Now they name that release's line, such as `@kindgi/cli@0.2` from 0.2.0. From a pre-release such as `0.3.0-rc.0`, npx finds no `@kindgi/cli@0.3` until 0.3.0 is out, so they say to name the CLI's own pre-release, `@kindgi/cli@0.3.0-rc.N`, whose number can differ from the runtime's (`npm view @kindgi/cli versions` lists them) ([Operate it](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/cloud-run.md#operate-it)).

- **Copying the runtime image says how long it can take:** the copy goes through the machine you run it on, every layer down and back up, and takes both platforms (amd64 and arm64), so on a slow uplink it can take most of an hour. `upgrade.sh` says so as it starts the copy, and in `--dry-run`, and so do the deployment pages ([Pull the runtime image](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/pull-the-runtime-image.md)).

- **A tool's error says why it failed:** a TypeScript tool whose `fetch` failed reported only `fetch failed`. The reason was in the error's cause, which no run showed. Now the message names up to three causes, as Node writes them: `fetch failed; caused by ConnectTimeoutError [UND_ERR_CONNECT_TIMEOUT]: Connect Timeout Error (attempted address: shop.acme.example:443, timeout: 10000ms)`. A refused connection names its addresses, and an error without a cause reads as it did. The message is masked, its causes included: a Kindgi token, and a URL's secret query or fragment value such as `?token=` or `X-Amz-Signature=`, become `[redacted]`, and the tool's own secrets and connection tokens are taken out first. The same holds for a check's `evaluate` and a pack file that fails to import. Rebuild the pack's image with this CLI to get it; a tool the server runs in its own process has it with this runtime ([When a call fails](https://docs.kindgi.com/v0.5.0/guides/tools/write-a-tool.md#when-a-call-fails)).

- **A tenant id that isn't a uuid is refused with a message that names the setting.** `KINDGI_TENANT_ID`, `--tenant` or the config file's `tenant` must be a uuid ([Self-host](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/self-host.md#3-build-sign-and-push-your-pack) makes one). Another id got past the settings check, then stopped the start at the runtime's first database write for the tenant, with a database error that named no setting. The runtime now refuses it as it reads its settings, before it connects to the database, and the message names the setting. The Cloud Run and Azure Container Apps modules refuse a `seed_tenant_id` or `seed_user_id` that isn't a uuid at plan.

- **`kindgi artifacts download` writes the file only once the whole file has arrived.** It writes to a new file next to the one it saves, and moves it into place once the stream has ended and its sha256 is the one the server sent. Before, a download that stopped part way left a cut-off file under the name, nothing compared the sha256, and with `--force` the file already there was overwritten as the download began. Now a download that fails says the file wasn't written and leaves no file, and the file already there stays as it was. Without `--force`, a file that turns up under the name during the download is kept too. The `size` and `hash` it prints are those of the bytes written, and a name of `.` or `..` from the server gives the file the blob id. [Keep files with a run](https://docs.kindgi.com/v0.5.0/guides/runs/keep-files-with-a-run.md#find-and-download-them) says what an interrupted download can leave behind.

- **One Ctrl-C stops `kindgi artifacts download`.** Before, the first Ctrl-C did nothing, and only a second one, a forced exit, stopped it, which could leave a half-written file behind. Now the first Ctrl-C, or `SIGTERM`, stops it, whether it's still waiting for the server or part way through the file. It leaves no file, the file already there stays as it was, and it says `kindgi artifacts download stopped: no file was written.` and exits with code 130.

- **A write sent with an `Idempotency-Key` that can't be checked against its key no longer runs.** When the database that keeps the keys fails before such a write runs, the answer is `503 idempotency-store-unavailable` with `Retry-After: 5`. Before, it ran as if it had no key, so a repeat could run it again. Now nothing runs, so a retry with the same key is safe, and the Python client retries it on its own. When the write ran but its answer couldn't be kept, its answer stands, and a repeat within 30 seconds gets `409 idempotency-key-in-flight`, then runs again. Each such failure is now logged ([Retry a start safely](https://docs.kindgi.com/v0.5.0/guides/runs/retry-a-start-safely.md#how-it-works)).

- **The system API's tenant reads answer `503` when the database is too busy.** `GET /v1/system/tenants` and `GET /v1/system/tenants/{tenantId}`, when the database's statement or lock timeout or the wait for one of the server's connections ended them, answered `500`. They now answer `503` ([`database-busy`](https://docs.kindgi.com/v0.5.0/reference/errors.md#database-busy)) with `Retry-After: 5`, as other reads do.

- **A deploy that failed after publishing its tools no longer leaves its image undeployable.** Its rollback unregisters the tool versions it published, and a version number is used once, so the next deploy of the same image was refused with `400 validation-failed` until the pack was rebuilt under a new tool version. A deploy now reinstates an unregistered tool version its pack brings, as `kindgi tools reinstate` does, one its owner unregistered too. A rollback that can't undo one of its writes now logs a warning that names what it left as it was ([What unregistering a version does](https://docs.kindgi.com/v0.5.0/guides/agents/agent-and-tool-versions.md#what-unregistering-a-version-does)).

- **The access audit no longer records some allowed requests twice.** Listing or creating a project's judging rules or sites, or the tenant's sign-in providers, listing a project's judging queue, and reading one agent, flow, tool, guardrail, eval suite or MCP endpoint each left two identical rows in the access audit when allowed. Each is checked, and recorded, once.

- **A deploy refused because its pack's items belong to another project names a fix your runtime has.** `409 agent-project-mismatch`, and the `tool-`, `flow-`, `guardrail-` and `block-` ones, named a copy of the pack made with `kindgi init`, even where one pack service runs every deployment, the default, so a copy's code wouldn't run. There it now names an environment order: put both projects in one (`kindgi environments set`) and `kindgi promote` the build to the second. For projects in two orgs it names moving one, and on a runtime with neither, no fix ([A pack goes into one project](https://docs.kindgi.com/v0.5.0/concepts/projects-packs-values.md#a-pack-goes-into-one-project)).

- **Each tool call in a model's reply has an id of its own.** The agent loop keeps what it decided for a call (its guardrails' verdicts, its approval, that it ran) by the call's id. When Gemini gave a call a null or empty id, or one an earlier call in the reply had, and when a compatible server sent a Chat Completions call like that, the Gemini and OpenAI-compatible adapters now make one, and send it back with the call and its result.

- **`kindgi deploy --hosted` and `kindgi deployments get` say when they can't read where each value is set.** A read that failed printed nothing, as if nothing stood in the way. Now they print `Couldn't read where each value is set:` and why, such as the HTTP status and code. The deploy goes on, and `deployments get --json` is unchanged.

- **The `sample` template says it has three tools.** `kindgi init --template=sample` makes `echo`, `greet` and `fetch-httpbin`, and `kindgi dev` loads all three, but the pack's README, `package.json` and `kindgi.config.ts` said two.

- **The Python SDK's logger writes an error inside a field as it writes `err`:** its class's name, its message with known secret shapes masked, its code and its causes, without a stack. It was written as its text alone. A value that raises when it's read is written as `[unreadable]`, and never fails the log call.

- **A flow's step whose read of the database failed is left for the run's next resume.** An agent step, a loop body's step or a fan-out's branch whose read failed failed the run. Now the step is left: its `step.retry-scheduled` entry says `left: true`, it runs again at the run's next resume and uses one of its `retry.maxAttempts`, and a step left 3 times fails as if it had thrown. A step left in a subflow's child run leaves the parent's subflow node too: the child run waits for its parent's next resume, shows as running, and ends when its parent ends. Resuming it directly is refused with [`run-held-by-parent`](https://docs.kindgi.com/v0.5.0/reference/errors.md#run-held-by-parent).

- **A failed type fetch on a new database connection no longer ends the runtime.** When the database refused, reset or ended it, as in a failover or a restart, the request waiting on it now fails with the database's error, or runs once the connection connects again.

- **A database that closes every connection while the runtime starts now fails the start.** It exits with code 1 after the connect timeout, its log saying `Can't connect to the database at <host:port/db>: CONNECTION_CLOSED`, instead of reconnecting with no line and never finishing its start.

- **A database restart or failover no longer leaves the migrations, their lock or the vector index build waiting for a connection.** Each takes a Postgres connection of its own. If the database ended a session while one waited, it now gets the connection the pool opens again. If every start of that connection is closed, it fails as a query does, at whichever comes first: its pool wait (`POOL_WAIT_TIMEOUT`; 10 s by default, `KINDGI_DB_POOL_WAIT_TIMEOUT_MS`) or the 30 s connect timeout (`CONNECTION_CLOSED`, also when there's no pool wait), and the pool stops opening connections for it. If the server refuses or resets that connection's type fetch, it fails with that error. When the runtime shuts down and the pool takes longer than its 5 s to close, what still waits fails with `CONNECTION_DESTROYED` instead of never settling.

- **An MCP endpoint's tool sync no longer gives up when another replica syncs the same tool.** It failed after the lock wait, 30 seconds by default, and the endpoint stayed unsynced until it was registered again. Now it tries again for up to 15 minutes, and writes only for the endpoint's current registration.

- **A `KINDGI_TENANT_ID` with an upper-case letter is read in lower case,** as the database, a key and a console session hand it back, so every part of the runtime reads one tenant. A person whose user record an earlier runtime made under the upper-case id signs in through the tenant's identity provider as that person.

- **A file deleted from a Google Cloud Storage bucket that keeps old versions is now gone from all of them.** With `KINDGI_ARTIFACTS=gcs:<bucket>[/<prefix>]` on a bucket with Object Versioning, purging a deleted artifact under a retention policy, and purging a tenant, left the file's bytes in an old version. Both now delete every version of the file. The bucket's own soft delete still keeps a deleted version restorable for its window, as before.

- **A tenant purge stays under its `gcs:` prefix.** With `KINDGI_ARTIFACTS=gcs:<bucket>/<prefix>`, a purge lists, deletes and counts only the files under `<prefix>/`. Before, with the prefix `prod`, it also reached the same tenant's files under `prod-eu/`, as when two deployments share a bucket and a tenant id.

- **A delete that the bucket's retention or a hold refuses says why.** The error names the file, the retention's end and each hold: `<bucket>/<key> is locked by GCS: a retention until <time> and a temporary hold`. When the runtime can't read the lock, it says the server lacks `storage.objects.get`. Before, it was Google's own message.

- **A fresh database's first start no longer logs that row-level security was "applied again".** The app role's first grant on `outbox_signal` was logged at info, as a repair. It's logged at debug now, as the rest of a first set-up is, and a repair stays at info. These lines, and a failed migration's error, no longer name an internal package: the error reads `migration failed in the … migrations:`, then why.

- **A service account's grant changes no longer cross:** two changes to one account's grants at once, such as a grant and an ungrant, two roles on one project, or a grant during `unregister`, could leave the access it has apart from what `kindgi service-accounts get` shows. Each change now waits for the one before it, across all of the runtime's instances, and `kindgi-runtime tenants repair` waits for them as it puts a grant back or takes one away.

- **Removing or changing access with an id that isn't in lower case no longer leaves it in place.** With an authorization store, removing or changing a project or team membership, or a team's grant on a project, now takes the old access away under each spelling of the person's id, and under the project's id in lower or upper case. A service account's grant or its removal takes the account's old roles on the project away under any spelling of the project's id. A service account's create or grant that names a project with an upper-case letter gives the role under the id in lower case, and a key minted naming the account with one acts as the account. Granting a service account a role it already holds writes it to the authorization store again, which also puts back a role a failed write lost.

## Changed

- **A guardrail's page with no description says what happens in sentences** in its header: "If an answer breaks it, the turn fails. Its answer isn't kept.", or "that's only recorded" for one that records ([Write a guardrail](https://docs.kindgi.com/v0.5.0/guides/guardrails/write-a-guardrail.md)).

- **Only agents and service accounts hold a run grant in the authorization model,** apart from a site person's `caller` on the site's project. `invoker` and `executor` on a project, `caller` on an agent, `executor` on a flow, `invoker` on a tool and `executor` on an eval suite admit no person or team now, and `caller` on a project admits no team. People run agents, flows and tools as editors, as before ([Authorization](https://docs.kindgi.com/v0.5.0/deploy/authorization.md)). Only a grant written straight into a tenant's OpenFGA store names a person or a team there: after the upgrade it grants nothing, so starting an agent or a flow answers `403` and a tool call in a run is refused. Give that person or team `editor` on the project, or on the resource itself, instead.

- **The CLI exits 3 when a command ran and its answer is no,** so a script or a CI step can stop on it. An error still exits 1 and a usage error 2, and the output is the same.

  - `kindgi agents promote --check` exits 3 for `gate-failed` and `needs-approval`.
  - `kindgi agents promote` exits 3 when the release gate policy refuses the version (it was 1) and when the promotion waits for an approval (it was 0).
  - `kindgi promote --make-live` exits 3 when a version it carries isn't pinned (it was 0).
  - `kindgi eval-runs start --wait` and `kindgi eval-runs rescore --wait` exit 3 when the run failed or was cancelled (it was 0).

- **`kindgi auth login` keeps another runtime's login.** When `~/.kindgi/config.json` holds a key for another address, or a key with no address beside it, login no longer replaces it without a word: it exits 2, names the login it keeps, and says how to have both: skip the login, and set `KINDGI_API_URL` and `KINDGI_API_TOKEN` to the new address and its key in the shell you use it from. `--force` replaces it, and a warning, under `--quiet` too, says whose key is no longer stored. A new key for the same address saves as before. A script that logs in to one runtime after another in the same home needs `--force` now ([Deploy your agent](https://docs.kindgi.com/v0.5.0/hosted/deploy-your-agent.md)).

- **A provider registration with an `adapter_config` key its adapter doesn't take is refused,** naming the key and the settings the adapter takes, or where a moved setting goes, and nothing is registered. Before, only Azure OpenAI and Bedrock refused one. A registration already stored with one still builds, without the key being read, where before the runtime skipped an Azure OpenAI or Bedrock one. `GET /v1/providers/{id}/check` lists the key in a new `warnings` array, and `kindgi doctor` shows the provider as a warning, with the steps to register it again. A custom adapter lists its keys in its entry's `configKeys` to get the same; one that doesn't takes any key ([A spec its adapter can't use](https://docs.kindgi.com/v0.5.0/guides/models.md#a-spec-its-adapter-cant-use)).

- **A model step's calls to read-only tools run at the same time, up to 8 at once.** A step's tool calls ran one after another. Now calls to tools that declare `mutating: false` that come one after another start together, and any other call runs on its own, in the order the model asked, as does a read-only call that may wait for another system's answer or for an app to be connected. The results reach the conversation and the model in that order. When an agent's read-only tools mustn't overlap, such as two lookups against an API that allows one request at a time, set its `parallelToolCalls: false` (`parallel_tool_calls=False` in Python) to run every call on its own, as before ([Calls that run together](https://docs.kindgi.com/v0.5.0/guides/tools/read-only-tools.md#calls-that-run-together)).

- **A create made with a key that has no person behind it makes no one its creator.** Creating an org, a team or a project, publishing an agent, or deriving an agent version with such a key, a service account's say, made the all-zeros id `00000000-0000-0000-0000-000000000000`, which no person has, its owner or admin. Now no one is, as publishing a flow, a tool, a guardrail or an eval suite already did, and the tenant's admins reach it through its parent, as before. A person's key still makes them its owner or admin. `kindgi-runtime tenants repair --all` takes away what an older runtime gave that id ([Who has access to a project](https://docs.kindgi.com/v0.5.0/deploy/authorization.md#who-has-access-to-a-project)).

- **A framework skill is a folder, and the long ones load in parts.** `kindgi skills sync`, `kindgi init` and `kindgi dev` copy each skill's `SKILL.md`, which a coding agent loads, and the files beside it, which it reads only for its case. The providers, TypeScript tools, MCP servers, fix-corrections and framework-feedback skills keep in `SKILL.md` what's true on every path, about 6 KB, and name the file for each case. A change you made to any of a skill's files keeps the whole skill as you left it until `--force`, and a file you added to its folder stays yours. The fix-corrections skill reads exit code 3 as an answer ([Keeping them current](https://docs.kindgi.com/v0.5.0/start/coding-agents.md#keeping-them-current)).

- **The Cloud Run module turns on Google's deletion protection for its database too.** `database_deletion_protection`, on by default, set only Terraform's, which a delete from the console, `gcloud` or the Cloud SQL API doesn't see. It now sets Google's as well. A deployment made with an earlier copy of the module gets it at its next apply, in place: if that plan says the instance `must be replaced`, stop, since a replace deletes the database. To delete the instance, set `database_deletion_protection = false` and apply first. An instance you serve from with `database_host_instance` isn't the module's: turn its protection on with `gcloud sql instances patch <instance> --deletion-protection` before it serves. Use the module as of the `@kindgi/sdk@0.3.0` tag ([Operate it](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/cloud-run.md#operate-it)). From 0.2.0, a deployment whose database has a private IP can't upgrade to 0.3.0 with `upgrade.sh` yet ([Known issues](https://docs.kindgi.com/v0.5.0/releases/0.3.0.md#known-issues)).

- **The API's OpenAPI document lists `503 database-busy` on every read.** A `GET` or `HEAD` whose database is too busy already answered `503` with `Retry-After`, and the document now says so on each, but `GET /health` and `GET /v1/openapi.json`, which never reach the database. A generated client's answers and errors don't change.

- **The Python client names a union's members by what tells them apart.** The numbered names are gone, with no alias. `Config`, `Config1` and `Config2` (an MCP endpoint's config) are `MCPEndpointConfigStdio`, `MCPEndpointConfigHttpSse` and `MCPEndpointConfigStreamableHttp`, and `Config3` to `Config5` (a registration's) are `RegisterMCPEndpointBodyConfigStdio`, `RegisterMCPEndpointBodyConfigHttpSse` and `RegisterMCPEndpointBodyConfigStreamableHttp`. `Content2` and `Content3` are `MCPPromptMessageContentText` and `MCPPromptMessageContentImage`, `Resume` and `Resume1` are `CompleteApprovalResultResumeOk` and `CompleteApprovalResultResumeFailed`, and `Baseline` and `Baseline1` are `JudgedComparisonSummaryBaselineRecorded` and `JudgedComparisonSummaryBaselineVersion`. `Config6` and `Config7` are `RegisterScheduleBodyConfig` and `PatchScheduleBodyConfig`, `Config8` is `RegisterWebhookTriggerBodyConfig` and `PatchWebhookTriggerBodyConfig`, one for each body, and `Baseline2` is `ComparisonCaseResultBaseline`. Code that imports an old name fails at the import: use the new one.

- **The Java client doesn't send back a value only a newer runtime knows.** A request body is checked against the values the client knows before it's sent, so an enum value, a union variant, or a union's text that a newer runtime sent is refused with a `ValidationException`. Before, the client sent it and the server decided. Reading one keeps it, as before, and so does `build()`, so an answer that carries one can still be copied and changed. Every enum class implements `OpenEnum` (`asString()`, `isKnown()` and `knownValues()`).

- **A `retry` guardrail sends a failed answer back to the model.** Before this release, a guardrail whose action is `retry` was only recorded, as `log-only` is, and the turn completed. Now a failed `retry` guardrail sends the answer back to the model, with the check's reason, for a new one. `retry.maxAttempts` counts the answers it fails, the first one included (2 when left out). When they run out, the turn fails with `guardrail-violation`, and the error's `attempts` says how many answers the turn had. A failed `halt` guardrail still fails the turn at once, and a guardrail's counts are now of answers judged ([Send the answer back](https://docs.kindgi.com/v0.5.0/guides/guardrails/halt-or-record.md#send-the-answer-back)).

- **A read whose database fails answers `503` or `500`, not an empty list or `404`.** About 45 reads answered as if nothing were there when their database failed: among them the agent, flow and tool lists, gets and versions, the guardrail, MCP endpoint, env, secret, adapter and reviewer reads, the schedule and webhook trigger lists and their fires, and the eval suite and run reads. They now answer `503 database-busy` with `Retry-After` when the database is too busy for a `GET`, and `500` otherwise. A write that needed such a read answers `500` and writes nothing, where it answered `400` or `404`. A session that can't be looked up answers `500` (`503` on a busy `GET`), where a `401` signed the console out. A turn whose tools or guardrails can't be read fails, and a resume whose can't be read waits, as `run-version-unreadable`.

- **Reading a run's journal a page at a time reads only that page,** and following a run's events reads only what's new. Both read the whole journal each time, which grew with the run. The answers are the same.

- **`kindgi-runtime tenants repair` also takes away what a tenant's record doesn't back.** It finishes a person's or a service account's removal that stopped half way, and revokes a service account's tenant admin, membership or project role that its grants don't give, each recorded in the audit. `--dry-run` shows what it would put back and take away, per tenant, and changes nothing. Unregistering a service account again finishes one that stopped half way ([After each upgrade: tenants repair](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/serve-many-tenants.md#after-each-upgrade-tenants-repair)).

- **A runtime doesn't start on a database a newer version migrated.** It exits with code 2, and its log says `This database was migrated by a newer Kindgi version`, names the migrations it doesn't have, and gives the ways out. An earlier runtime served such a database with its own, older code. So during an upgrade, once the new version has migrated the database, an instance of the old one that starts refuses. After a failed upgrade, fix forward, or restore the backup from before the upgrade into a fresh database and serve that with the old version. Under `kindgi dev`, update the CLI, or start with a fresh database (`kindgi dev --reset`).

- **`kindgi-runtime tenants` commands that change a database run only on one at their own version.** `purge`, `repurge`, `repair`, `make-admin`, `admin-key`, `give-capabilities` and `reset-second-factor` exit `2`, and change nothing, on a database a newer version migrated (run them with that version), or on one this version hasn't migrated yet (start this version's server on it first; after a restore, run this version's `erasures replay` before `repurge`). Each holds the lock a starting server takes to migrate, so a server that starts while one runs waits for it ([Serve many tenants](https://docs.kindgi.com/v0.5.0/deploy/self-hosted/serve-many-tenants.md#after-each-upgrade-tenants-repair)).

- **The console hides what's retired.** The Agents, Tools and Flows lists, and every versions table, hide retired rows until **Columns → Show retired**, which says how many there are; the page's address keeps the choice. When every row is retired, the list says so. A tool or an agent whose every version is retired still opens to its page, which says so and lists its versions, so you can bring one back.

- **Every versions table reads like the tool's.** Agents, prompts, settings and policies list each version with its Latest, Retired and This version tags, their own columns, **Locked by** (what names that exact version, flows included), then Retire or Bring back.

- **The Agents and Tools lists say what uses each one.** For a tool, **In use** counts the agents whose newest version lists it, or a version pinned for the project, and the flows whose latest live version runs it. For an agent, it counts the flows that run it and the schedules and mail triggers, not paused, that start it. Past 1,000 agents or tools, a list says there are more.

- **Test cases retire by version.** A test set's page lists its versions, highest first, and an admin on it can retire one, or bring it back. A comparison runs the highest version left, comparisons already run keep their results, and the Test cases list hides a set whose every version is retired until **Show retired**.

## Known issues

- **A guardrail of kind `llm-judge` defined in a pack never judges.** The pack's index leaves out `judgeCapabilities`, even when the guardrail sets it, so the runtime refuses that guardrail on every turn it applies to, deployed or in `kindgi dev`. With action `halt`, each of those turns fails as a `guardrail-violation` whose `evaluationErrors` name the missing `judgeCapabilities`. With any other action the turn emits a `guardrail.error` event and the reply goes out unjudged; that includes `retry` at the final answer, but a `retry` guardrail set before or after a tool call or at a model step turns back each try there until its attempts run out, and then the turn fails. Fixed in [0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md).

- **In `kindgi dev`, a pack guardrail's `at`, `sees` and `budget` are ignored.** A guardrail set to run before or after a tool call, or at a model step, runs on the final answer instead, so a `before-tool` guardrail doesn't stop the call. With `sees: 'conversation'` it judges the current turn only, and no time limit applies to its check, since `budget.maxLatencyMs` and its defaults hold only away from the final answer. Fixed in [0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md).

- **On a deployed runtime, a guardrail whose check is the pack's own code can't run before or after a tool call or at a model step, even when its check lists that point in `points`.** It counts as a check that couldn't run (`check-point-unsupported`) and emits `guardrail.error`. With action `halt` the turn fails. With `retry` each try is turned back at that point (the call refused, its result withheld, or the reply sent back) until the guardrail's attempts run out, and then the turn fails. With any other action the turn goes on unjudged at that point. Guardrails that name a built-in check run at every point their check supports. Fixed in [0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md).

- **On a deployed runtime, redeploying a pack in which a guardrail changed only its `at` or `sees` keeps the guardrail's old `at` and `sees`.** A deploy keeps an already-registered guardrail whose definition hasn't changed, and that comparison leaves out `at` and `sees`. So the deploy succeeds, and the guardrail goes on running at its old point, with its old view of the conversation. A change to a field the deploy does compare, such as `action` or `config`, is refused as `guardrail-already-registered`. Fixed in [0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md).

- **On Cloud Run, `upgrade.sh` won't upgrade a 0.2.0 deployment whose database has a private IP.** That's a deployment with `database_private_network` set and `database_deletion_protection` `true`, the default. 0.3.0 turns on Cloud SQL's own deletion protection for the database instance, and while that change is pending, the plan can't read the instance's CA certificates, which the server gets in `KINDGI_DATABASE_SERVER_CA`. So the script's plan check stops with `PLAN CHECK: the plan changes more than this upgrade does`, naming `template.containers.env.KINDGI_DATABASE_SERVER_CA` on `google_cloud_run_v2_service.server`, and nothing is applied: the deployment goes on running 0.2.0, and the backup the script took before the plan stays where the script says. The script's hint about a var file left off the command doesn't apply here. Upgrade from 0.2.0 straight to 0.3.1 instead. Fixed in [0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md).

## Upgrade steps

- **On Cloud Run, from 0.2.0 with a database that has a private IP, upgrade straight to 0.3.1.** 0.3.0's `upgrade.sh` stops such a deployment before it applies anything ([Known issues](https://docs.kindgi.com/v0.5.0/releases/0.3.0.md#known-issues)), and 0.3.1's takes it from 0.2.0 to 0.3.1 in one run ([Kindgi 0.3.1](https://docs.kindgi.com/v0.5.0/releases/0.3.1.md#upgrade-steps)).

- **Six new migrations, all additive:** new tables (trigger versions, pinned flow and tool versions and their changes, and a queue that builds flow records) and new columns that are empty or have a fixed default, with nothing rewritten. The first start runs them, as it runs any other migration. Five of them take a short lock on the tables they change or point to: `kernel_runs` (three of them do), `kernel_journal`, `kernel_waitpoints`, `kernel_triggers`, `kernel_trigger_fires`, `orgs` and `projects`; the sixth only adds a table. The one that adds trigger versions keeps `kernel_runs`, `kernel_triggers` and `kernel_trigger_fires` locked while it reads every trigger fire, to check two new references and index them. Meanwhile runs, triggers and their fires wait, on a server of the earlier version still running too, so with many fires (`SELECT count(*) FROM kernel_trigger_fires`), upgrade at a quiet time. A long transaction holding one of those tables makes the start stop after about a minute, naming the session that holds it. End that session or let it finish, then start again. On Cloud Run, the old revision serves meanwhile; on a single server, such as the Docker setup, the runtime is down until the new start succeeds. After them, the runtime indexes the flow records without holding back writes to them.

- **`KINDGI_SEED_USER_ID` must be a uuid.** A runtime whose seed user id isn't one now refuses to start, with a message that names the setting: `KINDGI_SEED_USER_ID`, `--seed-user-id` or the config file's `seedUserId`. Outside `kindgi dev`, such a runtime used to start. Set it to a uuid before you upgrade. An upper-case uuid (macOS's `uuidgen` prints one) is now read in lower case: if yours is in upper case, the upgraded runtime makes a new seed user with the lower-case id, and the old one stays, with its grants and the keys minted for it. It's no longer the seed user. Mint keys for the new one, then revoke the old one's keys and, with authorization on, take away its tenant admin ([People, API keys and service accounts](https://docs.kindgi.com/v0.5.0/deploy/people-and-keys.md)).

- **A `KINDGI_TENANT_ID` with an upper-case letter: move what earlier runtimes wrote under it.** 0.3.0 reads the id in lower case. Stored files and access an earlier runtime wrote under the id as you set it stay there, and at start the upgraded runtime logs a warning that names the steps left, with your id filled in. Keep `KINDGI_TENANT_ID` as it is until they're done, and register a new identity provider only once every replica runs 0.3.0:

  1. With `KINDGI_ARTIFACTS` set, move the stored files. In a `gcs:` bucket:

     ```sh
     gcloud storage rsync --recursive gs://<bucket>/tenants/<id as set> gs://<bucket>/tenants/<id in lower case>
     gcloud storage rm --recursive --all-versions gs://<bucket>/tenants/<id as set>
     ```

     In a `local:` directory on a disk that tells case apart, as on Linux:

     ```sh
     rsync -a <dir>/tenants/<id as set>/ <dir>/tenants/<id in lower case>/
     rm -r <dir>/tenants/<id as set>
     ```

     On a disk that folds case, such as macOS's, there's nothing to move. Until the files move, those stored before the upgrade can't be downloaded, and a retention purge or an erasure passes them by.

  2. With authorization on (`KINDGI_OPENFGA_API_URL` set), once every replica runs 0.3.0, run `kindgi-runtime tenants repair --tenant <id in lower case> --old-spelling <id as set>` once. It moves the tenant admins to the id in lower case, writes the memberships, grants and parents the records hold, and deletes the rest under the old id. Check the ids it lists: someone whose tenant admin was taken away through the console before 0.3.0 can show as a tenant admin after it, so take it from them there. Until it runs, a tenant admin given under the old id keeps it on what was made before the upgrade, and the console doesn't list them.

  3. Set `KINDGI_TENANT_ID` in lower case, and restart.

- **A Gemini registration that set `adapter_config.thinkingLevels` stops using it.** After the upgrade the key isn't read, so its models think as Google sets by default, and `kindgi doctor` shows the provider as a warning. Move each `model=level` pair into that model's `thinking` as its `default`, then unregister the provider and register it again from its spec ([A registration that set `thinkingLevels`](https://docs.kindgi.com/v0.5.0/guides/models/gemini-on-vertex-ai.md#a-registration-that-set-thinkinglevels)). In code, `GeminiProviderOptions.thinkingLevels` and the `GeminiThinkingLevel` type are gone: set each model's `thinking.default` instead.

- **A run a trigger started before the upgrade loses its trigger's tools allowed unattended.** A run now reads that list, and a mail run its mailbox, from the trigger version it started under. A run started before the upgrade has no version, so after it, a call waits for a person's yes wherever a trigger's run with no list would wait: a tool that declares `needsApprovalWhenUnattended` or uses a person's connection, and in a mail run, a tool that changes something or uses any connection, not only the mailbox's. Let such runs end before you upgrade, or approve their calls as they come.

- **A trigger changed during a rolling upgrade gets a version that names no one.** While instances of the earlier release still serve, a change one of them makes to a schedule, webhook or mail trigger makes no version. The runtime records it as a version with the cause `unversioned-edit`, whose `createdBy` is absent: at the trigger's next fire, delivery or look at its mailbox, or just before its next edit or take-over, so that the edit's own version holds only its change.

- **A service account granted a project by an id with an upper-case letter: grant it again.** A project's id is a uuid, and 0.3.0 checks a project or team only under its id in lower case, as the API returns it. Before 0.3.0, `kindgi service-accounts create` or `grant` naming a project by an id with an upper-case letter, such as one from `uuidgen` on macOS, gave the account its role under that spelling. From 0.3.0 the account loses that access, though its grants still list it. Grant it the same role again, which also deletes its role under the old spelling: `kindgi service-accounts grant <service-account-id> --project=<project-id> --role=<role>`. If the account was the project's admin, what it did under that spelling stays there: the people and teams it added reach the items, such as agents, it made that way, and the project's own people and the tenant's admins don't, as in 0.2.0. Then, naming the project in lower case:

  1. Remove each person the account added to the project, then add them again if they should stay.
  2. Remove each team grant the account made on the project, then grant the team again if it should stay.
  3. Publish the account's items that name the project again, naming it in lower case. The project's member list doesn't show who was added under the old spelling: your own records do, or a read of the authorization store. For a project id in mixed case, neither all upper nor all lower case, removing those people and team grants doesn't take their roles under it: delete those in the authorization store. A path that names a project or team with an upper-case letter is still refused, as in 0.2.0.
