# Kindgi 0.2.0

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

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

There was no 0.1.11 release: everything its preview (`0.1.11-rc.0`) brought is in 0.2.0, so upgrade straight from 0.1.10.

## Security fixes

- **On a runtime without an authorization store, the tenant's settings are a tenant admin's to change,** as they are with one. Such a runtime checked nothing on them, so a member key, or anyone signed in, could change the model providers, policies, adapters, trusted signing keys and the sign-in provider catalog. Anyone else now gets `403 permission-denied`, and reading them stays open to everyone. The outgoing webhook endpoints and the compliance evidence need a tenant admin for reads too. Without a store, a tenant admin is the deployment's API token, an `admin` key, or a console signed in with either; a `member` key, a key limited to a project and a person signed in with an identity provider or an emailed link aren't. Deploying doesn't change.

- **The CLI's sign-in file is your account's alone:** `kindgi auth login` writes `~/.kindgi/config.json` `0600`, in a `~/.kindgi` that's `0700`. Before, both took the default mode, so with the usual umask another account on the same computer could read the API key. A file or folder an older CLI wrote is fixed the next time `kindgi auth login` runs: the new key goes into a new `0600` file that replaces the old one, so a program that already had the old file open can't read the new key through it. To fix yours now, run `kindgi auth login` again, or `chmod 600 ~/.kindgi/config.json && chmod 700 ~/.kindgi` ([Sign the CLI in](https://docs.kindgi.com/v0.3.1/hosted/deploy-your-agent.md#sign-the-cli-in)). The CLI's other files that hold keys are owner-only too: `kindgi key create` sets a `~/.kindgi/keys` folder that's looser than `0700` to `0700` (private keys were already `0600`), a new `.env.<name>` file that `kindgi env set` or `kindgi env pull` makes is `0600` (one that's already there keeps its mode), and so is `kindgi feedback`'s draft in the temp folder. `kindgi dev` writes `.kindgirc.json` and `.kindgi/dev/runtime.env`, which hold the dev runtime's tokens, as a new `0600` file each time too, where it wrote into the file that was there. A symlinked one stays a link, and the file it points to is the one replaced; `kindgi feedback`'s files in `~/.kindgi/feedback/` now do the same, where a plain file took a link's place. One of these files, or `~/.kindgi/config.json`, that isn't a regular file, or is a link to one such as `/dev/null`, is left as it is: for `.kindgirc.json`, `kindgi dev` says it couldn't write it, and the banner names `.kindgi/dev/runtime.env` as where the token is. On Windows, the modes don't change, and `.kindgirc.json`, `.kindgi/dev/runtime.env` and `~/.kindgi/config.json` are written in place, as before; `kindgi feedback`'s files are still replaced there.

- **A tool's error is masked before a run shows it.** What a tool's error says reaches the run's record, its events, the console and the model. A pack tool's own secrets of four characters or more are now taken out of it, as written or as a JSON string writes them, as its connection tokens already were. Then secret-shaped text is masked: a Kindgi token, a `Bearer` value, a URL's password and a URL's secret query or fragment value, such as `?token=` or `X-Amz-Signature=`. Before, an error that quoted one of them showed it there ([When a call fails](https://docs.kindgi.com/v0.3.1/guides/tools/write-a-tool.md#when-a-call-fails)).

- **An HTTP tool's or an MCP endpoint's credential no longer shows in a failed call's error:** where the service quotes back what the call sent, the runtime puts `[redacted]` in its place, in the call's result, the run's events and the logs. This covers an HTTP tool's `authorization.secretRef`, and an HTTP MCP endpoint's `secretRef`, `auth` (a Basic credential's `user:password`, and the password alone), OAuth access token, and a credential an earlier runtime let it keep in `config.headers`. Before, a credential with no known shape stayed in the error as the service wrote it ([Store the secret](https://docs.kindgi.com/v0.3.1/guides/tools/give-a-tool-a-secret.md#store-the-secret)).

- **A hard revoke on Azure Key Vault purges the secret where the vault allows it:** every version is disabled, the secret deleted, then purged. Before, it was only deleted, so Key Vault kept it recoverable for the vault's retention period with every version enabled, and setting the name again in Kindgi recovered it, old values and all. Where Kindgi can't purge, under purge protection or without permission to read and purge deleted secrets (`Microsoft.KeyVault/vaults/secrets/purge/action`), every version of the copy kept is disabled, and a `secrets` warning says why ([Revoking for good](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/secret-manager.md#revoking-for-good)).

## New

- **Improve can try a cheaper model or another temperature,** and keeps one only when the decisions stay right. Mark the keys of the agent's model-settings block as for a settings block: `temperature`, `maxOutputTokens`, and the model the turn prefers, `model` (with `providerId`), its `enum` listing the models to try. A change there is kept only when no correction the current version passes fails, and when it's better, or no worse and cheaper beyond the spread. A suggested change's `evaluation` says which (`rule`: `better` or `cheaper`), with `costUsd` and `decisions`. Name the model in one place: a version that names it in the block and in the agent too is refused at publish and at deploy ([Let it try a cheaper model or another temperature](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#let-it-try-a-cheaper-model-or-another-temperature)).

- **`kindgi proposals improve` drafts the agent's instructions:** `--tier=prompt`, with the `--provider` and `--model` that draft them and `--drafts` (1 to 5, 3 by default). The agent version must take its instructions from a prompt block it pins. `--class-weights` picks whose corrections an Improve run learns from, as for `kindgi eval-runs start` ([Draft the agent's instructions instead](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#draft-the-agents-instructions-instead)).

- **Ask what an Improve run would cost before you start it:** `POST /v1/proposals/improve/estimate`, with the body an Improve run would start from, answers `estimatedCostUsd`: the replay count times the version's mean cost over its recent completed runs, capped at `--max-cost`. It starts nothing. In the TypeScript client, it's `proposals.improveEstimate` ([Start an Improve run](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#start-an-improve-run)).

- **An Improve run started by hand works in its scope's project:** for a project or segment scope, that project; for the tenant or an org, `--pass-project=<project-id>` (`projectId` in the API and the clients), else the agent version's. So an Improve run scoped to another project can use test cases built from that project's runs ([Start an Improve run](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#start-an-improve-run)).

- **Improve under `kindgi dev`:** an Improve run, a draft and an evaluation work as on a deployed runtime, for the whole tenant. Making a suggested change live writes it into the pack's files, each block's new version and content into the block's own file, and `kindgi dev` reloads; an undo is done there, with git. When a block's file changed since the suggested change was made, the request is refused with `409 promotion-superseded`, naming the file ([Under `kindgi dev`](https://docs.kindgi.com/v0.3.1/guides/evals/propose-a-change.md#under-kindgi-dev)).

- **A pack brings its data blocks:** one JSON file per block in its `blocks/` folder, in the shape `kindgi blocks publish` takes, and a long template can have its own file (`templateFile`). `kindgi build` checks each block and refuses a pack whose blocks don't hold up, naming each file. `kindgi deploy` publishes them before the agents that use them, and an agent pins the pack's own version of each. A version never changes: a deploy that brings one the runtime holds with other content is refused, so give the block a new version. Under `kindgi dev`, blocks come from the pack's files only, and a publish there is refused with `409 registry-read-only`. A Python, Java or Scala pack with blocks needs a `kindgi` package or `com.kindgi:kindgi-pack` that reads them: `kindgi build` refuses one from before blocks, naming the release to upgrade to ([Keep them in the pack](https://docs.kindgi.com/v0.3.1/guides/evals/change-a-prompt-or-setting.md#keep-them-in-the-pack)).

- **Edit a settings block as fields, and update its agents in one step:** each key gets a field from the block's JSON Schema (a choice, a number within its limits, a checkbox, a text) or, where none fits, a JSON box, and **Edit as JSON** goes back to the whole value as JSON. One button publishes the block, then derives a new version of each agent in the project that uses it, and pins it or asks for approval where the release gate policy and your access allow. Its label says which: **Save and pin**, **Save and ask for approval** or **Save and derive versions**. **Retry the failed steps** picks up where a step failed.

- **A correction can say what the agent should have done:** the tool calls it should have made, with the arguments that matter, and the tools it shouldn't have called (`kindgi judgments add --correction`, runtime 0.2.0 or later). When you try a version on test cases built from the run, it checks what that version does against it ([Say what it should have done](https://docs.kindgi.com/v0.3.1/guides/evals/judge-a-runs-output.md#say-what-it-should-have-done)).

- **`kindgi judging-rules` and `kindgi judging-queue`:** manage a project's judging rules, and the queue of runs they pick for a person to correct, from the terminal. `queue-recent`, or `create` with `--queue-last` or `--queue-since`, catches a rule up on runs that ended before it was there: `POST /v1/projects/{projectId}/judging-rules/{ruleId}/queue-recent` in the API, and `queueRecent` (`queue_recent` in Python) in the clients ([Queue the runs to correct](https://docs.kindgi.com/v0.3.1/guides/evals/judge-a-runs-output.md#queue-the-runs-to-correct)).

- **The console shows which runs are corrected, and a new rule can queue recent runs:** the runs list has a **Corrected** column and a corrected run's page a **Corrected** badge. Someone who can correct runs gets **Next to correct**, the next open run in the project's **To correct** queue. A new judging rule can **Also queue its recent matching runs for correction now**: **How many recent runs**, 1 to 1000, 100 by default ([Queue the runs to correct](https://docs.kindgi.com/v0.3.1/guides/evals/judge-a-runs-output.md#queue-the-runs-to-correct)).

- **Your coding agent can fix what the experts corrected:** the new `kindgi-fix-corrections` skill, which `kindgi init` and `kindgi skills sync` install, pulls an agent's runs marked wrong, sorts each correction to where it belongs (a tool's code, a setting, an approval rule or the agent's instructions) with a pack test for each, publishes a new version and asks a person to make it live. It never raises a limit or drops a check ([Fix what the experts corrected](https://docs.kindgi.com/v0.3.1/guides/evals/fix-what-experts-corrected.md)).

- **A suggested change names the corrections it rests on:** its `evidence.judgmentIds` lists up to 100 of the ones on the cases it learned from, and `evidence.judgmentCount` says how many when there are more. Each item of a test case lists its corrections in `judgmentIds` ([Whose corrections it learns from](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#whose-corrections-it-learns-from)).

- **An Improve run refused for having nothing to work on says why:** its `400 validation-failed` carries `details.reason`, `no-tunable-keys` or `no-pinned-prompt-block`, for a client to match ([Mark what Improve may change](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#mark-what-improve-may-change)).

- **Schedule Improve in the console:** **Schedule Improve**, on the project's **Schedules** page or on an agent's **Suggested changes** page, makes the same schedule as `kindgi schedules create --improve`, with the same defaults: what to improve, what better means, experts' corrections only, the scope, the cron (at most once an hour) and its time zone, the counts that start an Improve run, and what each run and each month may spend. It shows to someone who can change the project's schedules and start Improve runs ([Schedule Improve in the console](https://docs.kindgi.com/v0.3.1/guides/runs/run-on-a-schedule.md#schedule-improve-in-the-console)).

- **An agent's page shows its Improve loop:** after **Recent runs**, each section with its newest five: **Corrected runs**, with **To correct**; **Test cases**; **Improve runs**; and **Suggested changes**, which opens the agent's **Suggested changes** page ([In the console](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#in-the-console)).

- **The runtime image is pulled with your license key, from `registry.kindgi.com`.** The image is `registry.kindgi.com/kindgi/runtime`, tagged with each release's version, and your license key is its password, with the username `kindgi`. There's no separate pull credential. A production key and a free non-production key pull the same image, until 14 days after the key's expiry date; a revoked key stops pulling within a few minutes. The Cloud Run and Azure Container Apps pages copy the image into your own registry by digest, so a key's expiry never stops a deployment ([Pull the runtime image](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/pull-the-runtime-image.md)).

- **Sign in at access.kindgi.com with your email address,** as well as with GitHub. It emails a 6-digit code that works for 10 minutes, and gives you the same free non-production key. If your address is your GitHub account's primary verified email, either sign-in gives you the same key ([Sign in with your email address](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/pull-the-runtime-image.md#sign-in-with-your-email-address)).

- **Everyone gets their own key, at a company too.** A company address's key is named after the company's domain, such as `acme.com`, so the name the runtime prints at start is safe in shared logs, and one person's key can be revoked without touching anyone else's. If your company had one shared key, your next sign-in gives you your own, and the shared one keeps working until it expires ([Your key's name](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/pull-the-runtime-image.md#your-keys-name)).

- **Renewing a key for someone who signs in by email:** `kindgi license enroll --for` takes the `em-` id their key page at access.kindgi.com shows, as well as a GitHub login, and its help and messages say so ([Renew the license key](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/renew-the-license-key.md)).

- **`kindgi auth registry --key-from <store>`** reads the license key from where a deployment keeps it, as `kindgi license renew` does: `file:…`, `env-file:…#NAME`, `gcp:…`, `azure:…` or `aws:…`. So a deployment's own key logs in without passing through your shell. It's for `registry.kindgi.com` only ([`kindgi auth`](https://docs.kindgi.com/v0.3.1/reference/cli/auth.md)).

- **Cloud Run module: how many backups, and where:** `database_backup_retention`, `database_point_in_time_days` (at most 7 on `ENTERPRISE`, the module's edition, and at most as many days as backups kept) and `database_backup_location` (a region or a multi-region, `us`, `eu` or `asia`; a zone is refused at plan). Without them, Cloud SQL keeps 7 daily backups and 7 days of transaction logs, in the multi-region nearest the instance, which can be in another country. Setting them on a running deployment changes the instance in place, and the backups taken before stay where they were. They set the module's own instance only: with `database_host_instance`, they're refused at plan ([Cloud Run](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md)).

- **A lost first admin key, minted again:** for a tenant created without `firstAdmin`, `kindgi-runtime tenants admin-key` revokes its "First admin" service account's live keys and mints one key carrying what the first did, printed alone on stdout. It's refused when one of those keys has a use recorded, unless `--revoke-used`. `--dry-run` says what it would change, and each revoke and the mint are in the tenant's audit log, by `system`, with the `--reason` given ([Serve many tenants](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/serve-many-tenants.md)).

- **Java client: the helpers the TypeScript and Python clients have.** `Comparisons.comparisonOf(run)` reads a comparison eval run's result, typed ([Compare a version on a test set](https://docs.kindgi.com/v0.3.1/guides/evals/compare-an-agent-version.md)). `Webhooks.verify` checks a webhook Kindgi sends, and `Webhooks.parseEvent` reads its typed event ([Verify a webhook](https://docs.kindgi.com/v0.3.1/guides/webhooks/verify-a-webhook.md)). `SignedExports.verify` checks a signed export with the JDK's own crypto, so there's nothing extra to install ([Export signed evidence](https://docs.kindgi.com/v0.3.1/guides/observability/export-signed-evidence.md#check-an-export)). Scala apps call them from the Java client.

- **Java client: the sign-in setup message.** `SsoHandoff.forProvider` builds what an admin sends whoever runs their identity provider: the OIDC or SAML URLs, what to send back, and a preset's console steps (Google, Entra, Okta, Keycloak). It's the same text `kindgi sso providers start` and the console show ([Get what to send IT](https://docs.kindgi.com/v0.3.1/guides/sso.md#1-get-what-to-send-it)), with no network and no dependencies, for a JVM app with its own setup screen.

- **Java and Scala: a tool can be an HTTP request, declared as data.** A tool can end with `.http(spec)`, an `HttpToolSpec` (method, URL template, headers, authorization from a secret or a connection, request body, success statuses) in place of a handler. The runtime makes the request; calling the tool in the JVM throws `UnsupportedOperationException`. `{placeholders}` may sit in the URL's path and query, never its host or port, and in a text body, each filled from the input, as in TypeScript's and Python's HTTP tools ([Declare the request](https://docs.kindgi.com/v0.3.1/guides/tools/http-tools.md#declare-the-request)).

- **Python, Java and Scala: a test fills a tool's env as a run does.** `ToolContext.for_test(tool=...)` (Python) and `ToolContext.forTest(tool, env, secrets)` (Java and Scala) give a context whose `env` is what the tool declares, from the test's values or each schema's default ([Give a tool per-project values](https://docs.kindgi.com/v0.3.1/guides/tools/give-a-tool-env-values.md#test-it)).

- **A tool can pause its run until another system answers.** A TypeScript or Python pack tool that declares `pauses` hands another system its call's link, and returns `ctx.pause()`. That system posts its answer to the link, as JSON, with no key: the answer becomes the call's result, and the run goes on. A pause waits 24 hours by default, 24 days at most. `kindgi runs resume` names the wait and exits `5`, and `--output` answers it by hand; from an app, `runs.resume` answers it, and `runs.get` lists it under `waitingFor.tokens`. The link is made from `KINDGI_PUBLIC_URL` ([Wait for another system's answer](https://docs.kindgi.com/v0.3.1/guides/tools/wait-for-another-system.md)).

- **A schedule, webhook trigger or mail trigger can name tools its runs call without waiting for a person's yes.** A tool that asks for one when no person started the run (`needsApprovalWhenUnattended`, any call with a person's connection, reads included, or, in a mail's run, a change or a use of the run's own mailbox) can be listed in the trigger's `toolsAllowedUnattended`, by id, with `--allow-unattended=<tool-id>` on `kindgi schedules create|update`, `kindgi webhooks register|update` and `kindgi mail-triggers create|update`. It lifts only that wait: the agent's and the tenant's approval rules still apply, and the run acts as the trigger's owner. Setting it takes `admin` on the project, a take-over keeps it, and each change is recorded as `trigger-tools-allowed-unattended-set`, on legal hold ([Tools allowed unattended](https://docs.kindgi.com/v0.3.1/guides/approvals/ask-before-a-tool-runs.md#tools-allowed-unattended)).

- **Start runs from new mail:** a mail trigger watches a Gmail or Outlook mailbox, its inbox or one folder or label in it, and each new mail starts a run of an agent or a flow, with the mail and the rest of its thread as the run's input. A trigger on a person's mailbox reads its owner's own mail, so it's personal. One on the company's mailbox takes a project admin, and is shared when a service account owns it. Its runs act as its owner. Mail is text from anyone, so in a run a mail started, a call that changes something, or that uses the run's own mailbox, waits for a person's yes unless a project admin lists the tool in the trigger's `toolsAllowedUnattended`. A Microsoft 365 shared mailbox or a Google group isn't supported yet ([Start runs from new mail](https://docs.kindgi.com/v0.3.1/guides/runs/start-runs-from-new-mail.md)).

- **`kindgi mail-triggers`, `/v1/mail-triggers` and the clients:** `create`, `list`, `get`, `update`, `pause`, `resume`, `take-ownership` and `unregister`, as for schedules and webhooks, and `fires`, which lists a trigger's mail: the run each started, why one waits behind an earlier mail of its thread, or why it was skipped, refused or failed. A trigger's `statusReason` says why its mail starts no runs (`connection-missing`, `owner-refused`). The TypeScript client has `client.mailTriggers` and the Python client `client.mail_triggers`. A run a mail started says so: `startedBy` and `trigger.kind` are `mail`. `kindgi exports create` writes the mail triggers and their fires ([`kindgi mail-triggers`](https://docs.kindgi.com/v0.3.1/reference/cli/mail-triggers.md)).

- **Mail triggers in the console:** a project's **Mail triggers** page lists each one with the mailbox it watches, why it isn't reading mail when it isn't, what it starts and its last mail, and **New mail trigger** makes one. A trigger's page shows its last 50 mails and what came of each, and pauses, resumes or removes it or changes its owner. An approval a mail's run waits on shows **The mail it started from**, as plain text, above the tool call, and the run names its trigger under **Started by** ([In the console](https://docs.kindgi.com/v0.3.1/guides/runs/start-runs-from-new-mail.md#in-the-console)).

- **The `kindgi-using` skill finds a mail trigger in the console:** asked what the support inbox did with a mail, it links the trigger's page, with its latest mails, what happened to each and the run it started, and the list of every run the trigger started ([In the console](https://docs.kindgi.com/v0.3.1/guides/runs/start-runs-from-new-mail.md#in-the-console)).

- **Connections: a tool acts in another app with an account someone connected.** An admin registers the company's own Google OAuth client, Microsoft Entra app, GitHub App or LinkedIn app once, its secrets by reference. People connect on the app's own sign-in page, from the console's **Connections** page or from a run that waits for them, and the app's token goes into your own secret store. A tool declares the app and the permissions it needs (`needsSpec.connections`), and each call gets the connection's current short-lived token in `ctx.connections`: the model never picks an account. In a run no person started, a call with a person's connection, a read too, waits for that person's yes. A runtime offers connections with `KINDGI_PUBLIC_URL`, `KINDGI_ENV` and a secrets store, so `kindgi dev` doesn't ([Connect your apps](https://docs.kindgi.com/v0.3.1/guides/connections.md)).

- **Register the company's apps in the console:** a tenant admin registers, changes and removes them on the workspace's **App registrations** page. It shows the addresses to give each app: the **Redirect URI** (a GitHub App's **Callback URL**), and GitHub's **Webhook URL** or Google's **RISC receiver (optional)**, where the app says when access ends.

- **An app can tell the runtime when access ends,** at the registration's `noticesUrl` (`POST /v1/connection-notices/{tenantId}/{appId}`), which must be reachable from the internet; each notice is checked by its signature. Uninstalling the GitHub App removes its connections, and suspending it makes them need reconnecting until it's lifted. With Google's Cross-Account Protection, a person's withdrawn access or disabled account removes their connections at once, not at the next renewal ([Notices from the app](https://docs.kindgi.com/v0.3.1/guides/connections/reference.md#notices-from-the-app)).

- **An approval for a call with a person's connection is that person's alone to decide:** they need a reviewer seat at its rank or above, or the call fails at once with `connection-approver-not-a-reviewer`; add them on the **Reviewers** page. A tenant admin sees it but can't decide it (`403 named-approver-required`), and it never escalates: undecided at its deadline, it expires. The console shows the account under **As**, and the API has `approver` and `connection` ([Approvals](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md#approvals)).

- **Connections are in the audit log, on legal hold:** each connect and reconnect (`connection-created`), removal (`connection-removed`), and app registration made or changed (`connection-app-set`) or removed (`connection-app-removed`). With `KINDGI_COMPLIANCE_CLASSIFIER=shipped`, they're signed and kept as a secret's change is. An event names the account by a hash, not by its address ([On legal hold](https://docs.kindgi.com/v0.3.1/deploy/retention.md#on-legal-hold)).

- **A LinkedIn connection needs reconnecting every 60 days:** LinkedIn gives a self-serve app no refresh token, so the runtime marks the connection as needing reconnecting just before its access ends (`access-expired`). A run its owner started then waits for them to connect again; any other run's call fails with `connection-missing` ([When LinkedIn ends access](https://docs.kindgi.com/v0.3.1/guides/connections/linkedin.md#when-linkedin-ends-access)).

- **Ready-made tools for Gmail, Outlook, GitHub and LinkedIn** in `@kindgi/connections`, each included under the id your pack names: `gmailSend`, `gmailMessagesList`, `gmailMessageGet`, `gmailReply`, `outlookSend`, `outlookReply`, `outlookMessagesList`, `outlookMessageGet`, `githubIssuesList`, `githubLabelsList`, `githubIssuesAddLabels` and `linkedinPost`. To write more, `appFetch` calls an app's API with a call's connection, paced by the app's rate limits ([Use a connection in a tool](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md)). A tool imports its connection names, such as `ConnectionRejectedError`, from `@kindgi/sdk/define`, as it does its other names.

- **Read and answer mail with the ready-made tools:** `gmailMessagesList` lists or searches a Gmail mailbox, newest first, and `gmailMessageGet` reads one message whole, in the same shape as `outlookMessageGet`: its thread id, the first 20 people of each of To, Cc and Reply-To, its text and its first 20 attachments' names, types and sizes. `gmailReply` and `outlookReply` answer a mail in its thread, only to the one address the mail asks for, its Reply-To or else its From: a reply to any other address is refused, naming the right one, and nothing is sent. In a run no person started, a reply waits for a yes, as a send does. Each mail tool takes `as: 'company'` to use the company's mailbox, which a tenant admin connects by signing in as that mailbox; give such a tool its own id. A mail's text keeps where its links go, without their tracking parameters ([Ready-made tools](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md#ready-made-tools)).

- **A tool can say its results are text from outside:** `resultIsOutsideData: true` (`result_is_outside_data=True` in Python, `.resultIsOutsideData(true)` in Java and Scala) says the tool's results are text nobody checked, such as a mail someone sent. An agent then reads each of its results as data, not instructions, in that turn and in every later one. It changes what the model is told, not what the tool may do. `GET /v1/tools` shows it, and every ready-made mail read sets it ([Results from outside](https://docs.kindgi.com/v0.3.1/guides/tools/read-only-tools.md#results-from-outside)).

- **`kindgi connections` and `kindgi connection-apps`:** connect, list and disconnect accounts from the terminal ([`kindgi connections`](https://docs.kindgi.com/v0.3.1/reference/cli/connections.md)), and register the company's apps, for tenant admins: `add google`, `add github`, `add linkedin` and `add microsoft --tenant=<directory>` (`oauth2.tenant` in the API) ([`kindgi connection-apps`](https://docs.kindgi.com/v0.3.1/reference/cli/connection-apps.md)). `connect` prints the link to open and, at a terminal, waits until the connect finishes. `--quiet` prints the link alone.

- **`kindgi runs resume` names a connect the run waits for:** the app, what it asks for and the link to connect from, with when the link stops working. It exits `6` when that's all the run waits for. `kindgi runs get` prints the same line, and `runs.get` answers it under `waitingFor.connections` ([When the connection is missing](https://docs.kindgi.com/v0.3.1/guides/connections/use-a-connection.md#when-the-connection-is-missing)).

- **`kindgi init --template=follow-up`:** an agent that writes a follow-up mail from your notes and sends it from your own Gmail, each send approved first.

- **Connections in the API and the clients:** `GET /v1/connection-entries` lists the apps a company can connect, with what each permission allows in plain words, the directory a registration signs in at, and what a disconnect withdraws at the app. `/v1/connection-apps` registers the company's apps, for tenant admins, and `/v1/connections` starts a connect, lists connections and disconnects them. The TypeScript client has them as `client.connectionEntries`, `client.connectionApps` and `client.connections`, and the Python client as the same resources ([Connections reference](https://docs.kindgi.com/v0.3.1/guides/connections/reference.md)).

- **`kindgi exports create` exports connections:** the tenant's app registrations (`connection-apps.jsonl`) and its connections (`connections.jsonl`), removed ones too. Neither holds a token, and a registration names its secrets by reference only ([`kindgi exports`](https://docs.kindgi.com/v0.3.1/reference/cli/exports.md)).

- **`@kindgi/connections-inmemory`, for development and tests:** an in-memory connection binding and resolver, with a fake app in place of Google's or GitHub's servers. A test connects without a browser, and hooks make the fake app refuse a code, fail a renewal, withdraw a grant or send a notice. It refuses `NODE_ENV=production` unless `productionSafe`.

- **The skills know connections:** the tool-authoring skills (TypeScript, Python, Java and Scala) say how a tool declares the connection it needs, reads the call's token, and refuses with the connection's name when the app answers 401; the TypeScript one points at the ready-made tools and `appFetch`. The `kindgi-using` skill knows an approval addressed to one person, and hands over the link to connect from when a run waits for a connect (`kindgi runs resume` exits `6`). The TypeScript getting-started skill names the other provider presets, as the Python, Java and Scala ones do.

- **Your coding agent can upgrade a self-hosted deployment:** the getting-started skills (TypeScript, Python, Java and Scala) say how, and load for it. There's no migration script: the runtime brings its database up to date when it starts. A small dev deployment on Cloud Run, on runtime 0.1.8 or later, upgrades with the module's `./upgrade.sh`, `--dry-run` first: its plan takes every `*.auto.tfvars.json` in the module's folder, the pack's env among them, and `yes` applies exactly the plan it showed. Any other Cloud Run deployment upgrades by hand, staging first ([Upgrade by hand](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md#upgrade-by-hand)). One on Azure Container Apps follows its page's "Operate it" section, and any other the upgrade steps in [Operate a self-hosted runtime](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/operate.md) ([Your coding agent](https://docs.kindgi.com/v0.3.1/start/coding-agents.md)).

- **The Cloud Run `upgrade.sh` takes `--dry-run`:** it lists every change a real run would make, in order, and shows Terraform's plan for the new image, but changes nothing and never asks for `yes`. It doesn't run the upgrade checks against the database, since they run as jobs in your project ([Operate it](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md#operate-it)).

- **The Cloud Run `upgrade.sh` takes a server-only release:** a release such as 0.2.0.1 is a server fix for 0.2.0, with no `@kindgi/sdk@` tag of its own. Use the `upgrade.sh` as of the `@kindgi/sdk@0.2.0` tag, with `--to 0.2.0.1` ([Operate it](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md#operate-it)).

- **A run says whom it acts for:** `actsFor`, `user:<id>` or `service_account:<id>`, on `GET /v1/runs` and `GET /v1/runs/{runId}`, and in the TypeScript, Python and Java clients' `Run`. It's recorded when the run is created and never changes: a schedule's or a webhook's run acts for the trigger's owner, and a child run for its parent's. A public run token's view and the `run.finished` webhook don't carry it.

- **Your own links in the console:** `KINDGI_CONSOLE_LINKS`, a JSON object of label to URL, shows up to 8 links, such as your help desk, status page or data processing terms, on every page of the console, the sign-in page too ([Your own links in the console](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/sign-in.md#your-own-links-in-the-console)).

- **The runtime can check its database server's certificate against a CA you give:** `KINDGI_DATABASE_SERVER_CA_PATH`, a PEM file, or `KINDGI_DATABASE_SERVER_CA`, the certificates inline (runtime 0.2.0 or later). Every connection then uses TLS, and the certificate must chain to one of these CAs and be in date. It's for a database reached by an address its certificate doesn't name, such as Cloud SQL by private IP. OpenFGA's own connection to the database has a section of its own ([Check the database's certificate](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/database-tls.md#by-an-ip-address-with-the-servers-ca)).

- **A hard revoke says until when your secret manager keeps a recoverable copy:** Azure Key Vault keeps a deleted secret it can't purge for the vault's retention period. `DELETE /v1/secrets/{name}?hard=true` answers `recoverableUntil`, the `secret-hard-revoked` record carries it, and `kindgi secrets revoke --hard` prints it. Setting the name again before then purges that copy if Kindgi now can ([Revoking for good](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/secret-manager.md#revoking-for-good)).

- **`kindgi license enroll`, `kindgi license renew` and `kindgi init` in an existing app follow `--json`, `--raw`, `--table` and `--quiet`:** `enroll --quiet` prints the enroll line alone, the one you add at access.kindgi.com, and `renew --json` gives the key's `expiresAt` and `daysLeft`. With none of these flags, all three print what they printed before.

- **An agent's tool-call check is told the tool's version:** `authorizeToolCall` gets `toolVersion` with `toolId`, before the call and once an approval comes back, so an authorizer can apply a rule per version. It's optional, so an authorizer written without it keeps working.

- **Set how much each Gemini model thinks:** a Gemini provider's `adapter_config.thinkingLevels`, as `model=level` pairs (`"gemini-3.8-flash=low"`), or `thinkingLevels` in code, sends one of Google's four levels, `minimal`, `low`, `medium` or `high`, on every call to that model. Gemini 3.8 Flash thinks on every call, and its thinking bills as output and counts against `maxOutputTokens`. A model it doesn't name thinks as Google sets. An entry that doesn't hold up, such as a level below the least the model takes (`minimal` on Gemini 3.8 Flash), is refused when the provider registers ([Make it think less](https://docs.kindgi.com/v0.3.1/guides/models/gemini-on-vertex-ai.md#make-it-think-less)).

- **A shared trigger can run as its project:** `--run-as=project` (`runAsProject: true` in the API) on a schedule, webhook trigger or mail trigger makes its runs act as the project's own service account, made the first time with the editor role on the project. It takes a project admin. The account has no keys, its role can't be changed and it can't be named by id, and a trigger's `owner` says `isProjectAccount: true`. A tenant admin may unregister it, which stops its triggers until an admin takes each over, and deleting the project unregisters it ([A project's own account](https://docs.kindgi.com/v0.3.1/deploy/people-and-keys.md#a-projects-own-account)).

## Fixed

- **A turn whose `maxSteps` is above 32 stops at its 32nd model call:** a turn makes at most 32 model calls, one per pass of its loop. With a higher `maxSteps`, a model that still wanted a tool on the 32nd call ended the turn with no answer, and the failure didn't name the budget. The turn now fails with `budget-exceeded`, kind `steps` and `limit` 32 ([When a turn runs out](https://docs.kindgi.com/v0.3.1/guides/agents/budgets.md#when-a-turn-runs-out)), and a typed answer that doesn't fit on that call fails it with `output-schema-violation`. A `maxSteps` of 32 or less works as before. `defineAgent`'s doc comment now gives the real defaults: 8 steps, no cost cap and 120 000 ms.

- **`kindgi build` and `kindgi deploy` print each line as it happens:** the build server's log as the server sends it, Docker's for `kindgi build --local`, and, for a deploy, the wait for a hosted pack service. They used to print nothing until they were over. A build or a deploy that fails keeps what it printed, with the error after it; a build server's build that failed, or a deploy that stopped before it was sent, used to print only the error. The summary still goes to stdout at the end.

- **`--quiet` prints errors and warnings only, the same way in each command:** `kindgi deploy --quiet` used to print all of deploy's own lines, and `kindgi build`, `kindgi dev` and `kindgi exports create` left out their warnings or printed their progress. Each now keeps its errors and warnings, the CLI pin's warning and a rate limit's wait among them, and leaves out the rest. `kindgi dev --quiet` shows log records from `warn` up, not only `error`, and still says when the framework skills are out of date or couldn't be refreshed. About twenty more commands that still printed what they'd done, `secrets`, `env`, `key`, `init`, `mcp`, `webhooks`, `sites`, `upgrade` and `auth registry` among them, now leave it out, and a failed `kindgi runs start` run's warnings and error now show. What's shown only once prints alone on stdout under `--quiet`, as `kindgi connections connect`'s link does: `kindgi tokens create`'s secret, which it used to print nowhere, `kindgi webhooks register`'s signing secret, and `kindgi sites`' one-time code.

- **The 90-day limit is a preview release's, whatever its version:** the runtime's refusal and startup warning, `kindgi dev`'s warning, the console's license line and the description of `GET /v1/license`'s `preview` no longer say that it ends at Kindgi 1.0. A release still stops starting 90 days after it was released, with a warning from 30 days before, unless a production key exempts it.

- **The Cloud Run `upgrade.sh` waits for a new backup-bucket grant:** when the grant it makes on the backup bucket is new, the database export no longer stops with HTTPError 412 while the grant takes effect. It tries again every 15 seconds, for up to 7 minutes. If Cloud SQL still can't write to the bucket after that, it stops before changing the deployment and says how to check the grant.

- **`kindgi improve` and `kindgi eval` name the command you meant:** `kindgi proposals improve`, and `kindgi eval-runs` or `kindgi eval-suites`, where they printed only "Unknown command". An unknown command named like a JavaScript object's member, such as `constructor`, printed a `TypeError`; it now gets the usual message.

- **`kindgi deploy --dry-run` works on an unsigned build** (`kindgi build --skip-sign`): it shows the body it would send, less the signature's fields, and exits 0. It used to fail with "Internal error building POST body". Without `--dry-run`, an unsigned build is refused as before.

- **A TypeScript pack that installs with pnpm or Yarn no longer starts with a warning** that its pack service dropped `COREPACK_ENABLE_DOWNLOAD_PROMPT`. Rebuild the pack's image with this CLI to stop it; nothing else in the image changes.

- **`kindgi build --hosted`'s link for a pack over 30 MiB** goes to the docs for the CLI's release line, not always to 0.1's.

- **`kindgi init` in an sbt app reads `name` and `version` inside a one-line `.settings(…)`,** such as `lazy val root = (project in file(".")).settings(name := "acme-ledger", version := "1.2.0")`. It used to stop unless `--pack-id` was given, and to use `0.1.0` for the version. A setting inside a comment is no longer read.

- **A schema with a format bound compiles under `HOSTED_BOUNDS`,** as under `SELF_HOSTED_BOUNDS`: `formatMinimum`, `formatMaximum` and their exclusive forms, on `date`, `time` and `date-time` values. In strict mode such a schema was refused for an unknown keyword, and otherwise its bound was ignored.

- **`kindgi key create <id> --env <name>`'s note follows what the environment has:** it reads the `kindgi.config.ts` in the folder it runs in. When the environment already names this key (its `signingKey` is this key's file, and its `signerKeyId` is this key's id or unset), the note says `kindgi build --env=<name>` will sign with it, and there are no lines to add. Before, it always said to add `signingKey` and `signerKeyId`. When the environment names another key, the note shows the two fields it has now, then the lines to add. With `--home <dir>`, the `signingKey` to add is the file the key went to: its path from `~` when that's under `$HOME`, such as `'~/work/.kindgi/keys/<id>.pem'` for `--home ~/work`, and its full path otherwise. It was always `'~/.kindgi/keys/<id>.pem'`, where `kindgi build` found no key. The note's lines are written as `kindgi.config.ts` strings, so a quote or a backslash in a path is escaped ([Rotate a signing key](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/rotate-keys.md#rotate-a-signing-key)).

- **A tool's error says why it failed:** in TypeScript, a tool whose fetch failed used to report only `fetch failed`. The message now names up to three causes, as Node does (`TypeError: fetch failed; caused by ConnectTimeoutError [UND_ERR_CONNECT_TIMEOUT]: …`), and a refused connection names the addresses it tried. It shows wherever the failure does: the run's record and events, the console, the model's retry and the CLI, for a pack tool's handler, a check's `evaluate`, a pack file that fails to import and a tool run in the server's own process. An error with no cause reads as it did ([When a call fails](https://docs.kindgi.com/v0.3.1/guides/tools/write-a-tool.md#when-a-call-fails)).

- **A trigger can't be set to run as an owner that can't run what it starts:** with an authorization store, naming a service account (`--run-as`), running a trigger as its project, taking one over, or changing a schedule's or mail trigger's agent or flow is refused with `409 trigger-owner-cannot-run-target` when the owner can't run it, and nothing changes. The message says what fixes it, such as giving the account the editor role on the target's project. Before, the change was accepted, and then every fire was refused until the trigger paused ([Run as a service account](https://docs.kindgi.com/v0.3.1/guides/runs/run-on-a-schedule.md#run-as-a-service-account)).

- **With authorization on, a change answers once its own access is written, even behind a queue of older changes:** creating a project, publishing an agent or adding a member wrote one batch of up to 500 queued authorization changes before answering. With more of the tenant's older changes waiting, it could answer before its own were written, and the caller's next request could get `403 permission-denied` until the background worker caught up. It now keeps writing until its own are in, for at most 2 seconds; a slower write lets the answer go first, and the background worker finishes it.

- **A tenant's setup that fails after making its OpenFGA store deletes that store:** when writing the store's model, the tenant's first grants or the runtime's record of the store failed, the store stayed in OpenFGA with nothing naming it, and each retry made another. The setup now deletes it, unless the record of it may have been written, and names a store it can't delete, or keeps, in its error. A store left by a runtime that stopped during the setup stays in OpenFGA.

- **A flow step that waits inside a loop nested in another loop suspends the run:** such as an agent step whose tool call waits for a person's yes, in the body of a `loop` step that is itself in another's body. Before, the run failed at once with `Interrupted: the run's executor stopped …`, and answering the wait couldn't resume it. Now the run waits, and goes on where it waited.

- **A block a turn can't load answers `422`, not `500`:** `block-unresolvable`, for no version of a data block in the agent's range, a pinned version that's gone, the wrong kind, or no blocks on the runtime, answered `500`, as if the server had failed. It answers `422` now, as `tool-version-unresolvable` does. The TypeScript and Python clients still raise it as a `ServerError` with its `code`.

- **The runtime says when two MCP endpoints hold the same tool id:** endpoint `a` listing `b.c` and endpoint `a.b` listing `c` both make `a.b.c`. Each sync skipped such an id without a word, and a reference got whichever version its range picked. The log now warns `MCP tool id <id> is held by more than one endpoint at once`, naming each endpoint and its versions, and the runtime's start lines have a `⚠ MCP:` line with how many. Unregister the versions of all but one endpoint ([When a tool id is taken](https://docs.kindgi.com/v0.3.1/guides/tools/mcp-servers.md#when-a-tool-id-is-taken)).

- **A deployment with no tool and no check of its own gets no pack service** on a runtime that serves many tenants (`KINDGI_PACK_ROUTING=per-deployment`): its answer has no `endpoint`. Before, it got an endpoint that stayed `pending`, though the deployment was in place. One left pending by an earlier runtime is no longer shown ([Serve many tenants](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md#serve-many-tenants)).

- **A trigger's owner records are on legal hold, as a service account's are:** with `KINDGI_COMPLIANCE_CLASSIFIER=shipped`, `trigger-owner-set` (a trigger made to run as a service account, or taken over) and `trigger-changed-for-owner` (a project admin's change to a trigger a person owns) are kept for good, signed and exportable. Before, `trigger-owner-set` had no policy: it was kept, but left out of the compliance evidence and of signed exports. Records from before the upgrade join the compliance evidence and signed exports at the upgraded runtime's first start. A classifier file of your own needs both kinds added ([On legal hold](https://docs.kindgi.com/v0.3.1/deploy/retention.md#on-legal-hold)).

- **On a runtime without an authorization store, the People list says everyone works in every project:** each person's **Access** reads "Every project (this runtime doesn't check roles)", then any reviewer role, where it read "no role yet" for someone without one. With a store, it reads as before ([Without authorization](https://docs.kindgi.com/v0.3.1/deploy/people-and-keys.md#without-authorization)).

- **On the console's sign-in page, only the button you clicked says it's busy:** clicking **Continue** also turned **Sign in with an API token** into "Signing in…", clicking that turned **Continue** into "Continuing…", and **Sign in as seeded user** and an emailed link's **Continue as …** did the same. The others now keep their labels, disabled until it's done, and coming back with the browser's Back button no longer shows the page still busy.

- **Screen readers read console labels they used to skip:** a run's step status icons, the release gate's checks when you pin a version, **Add a person**'s outcome marks, a new API key's secret, the guardrail page's **What it caught** and each rule's trend card, and, when you set up a second factor, the key to type in if the authenticator app can't scan the QR code ("The key, to type in"). A rule's preview is read out as its answer changes. Nothing changes on screen.

- **The console and the runtime's root page link the docs of their own release line,** such as `https://docs.kindgi.com/v0.2/` for 0.2.0 and its candidates. The console's **Docs** links went to the 0.1 docs whatever its version, and **Add an identity provider**'s **Step by step** went to its guide in the unversioned docs. The root page's **Documentation** link (`GET /`) went to `https://docs.kindgi.com/`, which follows the newest release.

- **An Improve schedule counts only the corrections on the version an Improve run would start from:** the version pinned for its scope, else the latest. Before, an older version's corrections kept counting, so a schedule could start Improve runs for mistakes a newer version had already fixed ([Improve on a schedule](https://docs.kindgi.com/v0.3.1/guides/runs/run-on-a-schedule.md#improve-on-a-schedule)).

- **An interrupted Improve run ends as its evaluation says when it's picked up again:** one whose best change beat the recorded answers ends `proposed`, even if its suggested change was meanwhile sent for review, pinned or withdrawn. Before, it ended `nothing-found`, hiding a change waiting for a reviewer, and a run whose best change didn't beat them gave the reason "The hold-out comparison ended withdrawn."

- **The console says what pinning a suggested change will do:** **Ask to pin it** restates the result and the release gate policy's verdict, or says "No release gate policy covers this scope", where it said the version "goes through the scope's gate" even with none. When Improve needs a version pinned for every project first, it links to **The agent's pinned versions**, and deriving a version warns when none is pinned in the project, since its runs take the new one at once.

- **A Python tool or agent whose model has a pydantic tagged union deploys:** with `Field(discriminator=...)`, pydantic writes OpenAPI's `discriminator` beside the union's `oneOf`, and the runtime refused the tool's input or output, or the agent's typed answer, with `strict mode: unknown keyword: "discriminator"`. The SDK now leaves it out, each member's tag a `const` (an `enum` for several values) and `required`. A union that a callable `Discriminator` picks is sent as an `anyOf`, so a value that fits two members, which pydantic takes, is no longer refused.

## Changed

- **`kindgi dev`'s banners no longer print the dev token in full:** the `Token` line of the "Kindgi is up" block, and `--no-watch`'s `Bearer token` line, show it as `kgi_bt_…` and its last four characters, as the runtime's own startup line does, so a pasted dev log doesn't give a working token away. Each line says where the full token is: `token` in the pack's `.kindgirc.json`, with the API's URL as `apiUrl` ([Connect your app to the runtime](https://docs.kindgi.com/v0.3.1/start/call-from-your-app.md#connect-your-app-to-the-runtime)), or `.kindgi/dev/runtime.env` if that file can't be written. A token pinned with `--dev-token` in another shape shows only its last four characters. `--json` and `--raw` still print the token in full. The TypeScript and Python getting-started skills say where the token is too.

- **The runtime image moves from `quay.io` to `registry.kindgi.com`.** access.kindgi.com no longer gives a robot name and token for quay.io: you log in with your license key instead ([Log in to the registry](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/pull-the-runtime-image.md#log-in-to-the-registry)).

- **`kindgi auth registry` logs in to `registry.kindgi.com` with your license key:** the username there is `kindgi`, so it needs no `--username`, and the hidden prompt asks for the license key. A value that isn't a license key (one starts with `kgi_lk_`) is refused before anything logs in, and isn't shown. The hints in `kindgi auth registry`, `kindgi dev` and `kindgi doctor` name the license key, not a robot, when the image is on `registry.kindgi.com`. Another registry, such as a mirror, still takes `--username` and its own token.

- **A schedule or a webhook trigger is personal or shared, and its owner stays put.** One a person owns is personal: its runs act as them, with their connections, and only they or a project admin can change, pause, resume, fire or delete it. Anyone else is answered `403 permission-denied`. One a service account owns is shared: its runs act as the account, with the company's connections. Changing what it runs (its agent or flow and version, its input, its tools allowed unattended, a webhook's signing secret) takes a project admin, and the project's writers can still change the rest, and pause, resume, fire or delete it. No change moves the owner: only a take-over does. A change to a personal trigger by anyone but its owner is recorded as `trigger-changed-for-owner`. Without an authorization store, a project admin here is a tenant-admin key ([Shared or personal](https://docs.kindgi.com/v0.3.1/guides/runs/run-on-a-schedule.md#shared-or-personal)).

- **An Improve run counts everyone's corrections until there's a restricted judge class:** with no `classWeights`, an Improve run and an Improve schedule count every correction, each by its class's weight (`as-recorded`), until the test cases' project, its tenant, or the agent improved there has a restricted judge class. Then they count only the corrections under one (`restricted-only`), as before. A schedule decides at each run. One created before 0.2.0 kept an explicit `restricted-only`: recreate it without `classWeights` to follow the project ([Whose corrections it learns from](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#whose-corrections-it-learns-from)).

- **An Improve run's cost counts the runs that prove its best candidate,** on the cases it didn't search, and the current version's replays there. They run after the search, so they can take an Improve run past `--max-cost`, and an Improve schedule can reach its monthly cap sooner ([Start an Improve run](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#start-an-improve-run)).

- **A suggested change Improve drafted needs someone else to approve it when there's another reviewer:** with a second reviewer who could decide it, the person who asked to make it live can't approve it (`403 separate-approver-required`). A lone reviewer still can. A release gate policy's own `separateApprover` stays the floor ([Start an Improve run](https://docs.kindgi.com/v0.3.1/guides/evals/improve-an-agent.md#start-an-improve-run)).

- **An Improve run you start asks for its suggested change's review itself,** as a scheduled one does, in the name of whoever started it. While another reviewer could decide it, the person who started the run can't approve it (`403 separate-approver-required`). Before, its change waited until someone asked for the review, with `kindgi proposals request` or the console's **Request promotion**.

- **The release gate's messages use the product's words:** "The release gate policy refused …", "the test run", "this version" and "the live version", and experts' judgments. Its replay checks say "N tool calls were refused", not writes, since a refused call can be a read, and in the console a policy's **Refused writes** is **Refused tool calls**. The error codes, check names, fields and counts don't change.

- **The console's Improve and evals pages, and Improve's reasons, use plain words:** **Test sets** is now **Test cases**, the Quality tabs **Queue** and **Proposals** are **To correct** and **Suggested changes**, a policy's **Tool rules** are **Approval rules**, **Judge this run** is **Correct this run**, and the **Trends** tab says **Marked right** where it said **Yes**. An Improve run's `outcome.reason` and a suggested change's `hypothesis` say "the cases Improve learned from" or "didn't learn from", not the search or hold-out part, and a schedule's skip note counts experts' corrections, not trusted judgments. Routes, API fields and error codes don't change.

- **An Improve run's page says why it found nothing, and when to doubt it:** **Nothing measured** when nothing it tried could be scored, else **Nothing clearly better**, and where it stopped: at the limit on changes to try, on the cost budget, or with no further change to try. With fewer than 10 test cases picked, the Improve dialog says the figures may not hold, and a suggested change checked on fewer says to read them with care. A suggested change's status reads, for example, **Better on the test cases**, and a block's page marks what Improve may change **Tunable**.

- **A conversation you can't read answers `404 conversation-not-found`,** as if it didn't exist, where it answered `403`: reading it or its messages, closing or unregistering it, and starting a run in it that its participant rule refuses. A caller who can read it keeps the `403`, and the clients raise the not-found error in place of the forbidden one ([Hold a conversation](https://docs.kindgi.com/v0.3.1/guides/agents/conversations.md)).

- **A hard revoke is no longer called "cryptographic erasure":** it erases the value at Kindgi and hard-deletes it in a secret manager, and can't be undone, but backups and a secret manager's own retention may keep copies for a time ([Revoking for good](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/secret-manager.md#revoking-for-good)).

- **`kindgi dev` refreshes the pack's framework skills as it starts,** after an upgrade, where it only said they were behind. It works as `kindgi skills sync` does without `--force`: skills you edited are kept, and one you deleted stays deleted.

- **The `woocommerce` template's store agent takes its instructions from a prompt block the pack brings** (`blocks/store-agent-prompt.liquid`), so `kindgi deploy` publishes it with the agent, and an Improve run on the instructions can draft new ones. A change to them takes a new version within the agent's range.

- **The pages a person opens from a link are in the sign-in page's frame:** a link to connect an app, the page the app sends them back to, an approval's link (`/console/approvals/<approval-id>`), and signing out. They show Kindgi's band beside what the page says, with links to kindgi.com and the docs, the deployment's terms and privacy links when they're set, and its own links under the page ([Terms and privacy links](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/sign-in.md#terms-and-privacy-links)).

- **The console has its own pages for an error and for an address with no page:** "This page couldn't be shown", with **Reload** and **Open the console**, and "Page not found". On a page with the console's menus, they show in its place and the menus stay. Without the menus, such as at sign-in or on a link to connect an app, they're in the sign-in page's frame, with the deployment's terms and privacy links when they're set ([Terms and privacy links](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/sign-in.md#terms-and-privacy-links)). The error page doesn't show the error itself; the browser's console has it.

- **The Cloud Run and Azure Container Apps modules ask the pack's service whether it's ready every 2 s as it starts,** where they asked every 5 s, still for up to two minutes in all. On Cloud Run, a cold start's first tool call waits about 1 s for it, not about 2.5 s. It takes effect at your next `terraform apply` with the updated module ([Cloud Run](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md), [Azure Container Apps](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/azure-container-apps.md)).

- **On Cloud Run, the pack's service has CPU only while a call runs,** so the instance `pack_min_instances` keeps up bills at Cloud Run's lower idle rate between calls, and its CPU is boosted as it starts. It takes effect at your next `terraform apply` with the updated module ([Cloud Run](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/cloud-run.md#what-youll-have)).

- **`approvals.completeToken` is gone from `@kindgi/client`,** with its `CompleteTokenInput`, which `@kindgi/sdk` re-exported too. It had no route and always threw `not-yet-wired`. Code that imports `CompleteTokenInput` stops compiling; an approval's run resumes through `approvals.decide`.

- **A deploy that changes a tool but keeps its version is refused:** `409 tool-already-registered`, naming the fields that differ, and nothing is deployed. Before, the deploy answered ok, kept the definition the version was first published with, and ran the new code under it, so a new input schema, `mutating` or approval didn't take. Give the tool a new version. A new image of the same tool still deploys. Deploying again an image recorded before 0.2.0, whose tool the runtime kept with another definition, now gets the 409 too: give the tool a new version and build again ([Deploys make the new version for you](https://docs.kindgi.com/v0.3.1/guides/agents/agent-and-tool-versions.md#deploys-make-the-new-version-for-you)).

- **In TypeScript, `InvokeAgentError` declares every error an agent turn returns:** `run-journal-unavailable`, `hitl-rejected`, `hitl-cancelled` and `hitl-withdrawn` are now in the type, with their error classes, so a `switch` on `code` sees each. The codes and messages are unchanged. A `switch` that checks every case (with `never` or an `assertNever`) must now handle these four to compile. `resumeAgentTurn`'s `agent-version-mismatch` now carries `conversationId` ([Decisions](https://docs.kindgi.com/v0.3.1/guides/approvals/approvals-reference.md#decisions)).

- **The emails the runtime sends are in Kindgi's brand:** the sign-in link, an invitation, the notice that a recovery code was used or a second factor removed, and the reviewer email and its digest have an HTML part with the Kindgi band. An email with one link (a sign-in, an invitation, one approval) has it as a button, written out under it, and a digest links each approval as **Open it**. They load no image or font, so opening one contacts no server. Subjects and text parts are as before. An email whose HTML part can't be made goes as text only, and the runtime logs a warning ([Reviewer email](https://docs.kindgi.com/v0.3.1/guides/approvals/decide-an-approval.md#reviewer-email)).

- **A project's dashboard says whether it's live, and what's deployed:** the Production card is now a button in the header, **Testing mode** or **Live**, that says what that means and holds a tenant admin's **Go live** or **Move back to testing**. The **Kindgi Core** card names the pack and version deployed, or "No pack deployed to this project", and how many values aren't ready. Renaming the project keeps its slug unless you tick **Change its slug too**.

- **The console says Latest or Pinned for an agent's versions, not live:** an agent's **Live versions** page is **Pinned versions**, at `…/agents/<agent>/pinned` where it was `…/live`, with **Where it's pinned** and **Pin history**, and a suggested change's **Request promotion** is **Ask to pin it**. "Live" is kept for a project's mode. The API, the CLI and the docs still call a pinned version live.

- **An agent's, a flow's and a tool's pages show what each does:** an agent's page is about the version this project's runs use, with its **Input** and its **Available tools**, marked **Only reads** and **Needs approval**. A flow's **Steps** diagram zooms, goes full screen and opens each step's agent, tool or flow. A tool's page lists its **Input** and **Output** fields, **Used by agents** and **Used in flows**. **Register agent** and **Register tool** are hidden: an empty list says to deploy the pack, or, under `kindgi dev`, which folder.

- **A run's page folds a guardrail's reason:** the reason can quote what the guardrail blocked, such as a card number, and the run's failure and a flow step's error printed it in full. "A guardrail blocked the answer.", the guardrail and the code stay in view, and the reason is behind **Show why it was blocked**, as the blocked answer is behind **Show what was blocked**. The run's journal keeps it in each row's folded details.

- **The runtime starts faster when its database is busy:** its migration check at each start sends about half the statements it did when there's nothing to apply. Each table's row security, policies and grants are still checked at every start.

- **A Python tool's schemas are checked when it's defined, as the runtime compiles them:** `@tool` and `http_tool` raise `DefinitionError`, in the runtime's words, for an input or output schema, a dict or one derived from a model or a type, that the runtime would refuse: a keyword `json_schema_extra` adds, a `format` it doesn't know (pydantic's `"format": "path"` for a `pathlib.Path` field), `minLength` with no `"type": "string"`, or a `pattern` JavaScript's `RegExp` doesn't read, such as `(?P<name>…)`. `@tool` refuses the same in a schema under `needs_spec.env` or `needs_spec.secrets`, and an env default that isn't a string. Before, the deploy refused it ([Write a tool](https://docs.kindgi.com/v0.3.1/guides/tools/write-a-tool.md#the-tool)).

- **`kindgi build` refuses a Java or Scala tool's schema where the runtime would:** beyond unknown keywords, a keyword with no `type` it applies to (`minLength` with no `"type": "string"`), a `type` its context can't be, a `required` property nothing defines, an empty `enum`, a `$ref` that only leads back to itself, and a `pattern` the runtime's JavaScript `RegExp` refuses. Before, such a schema reached the runtime, which refused it, and a pattern went by Java's `Pattern`, so `(?i)abc` passed the build. The indexer refuses the same in a schema under `needsSpec.env` or `needsSpec.secrets`, and an env default that isn't a string ([Write a tool](https://docs.kindgi.com/v0.3.1/guides/tools/write-a-tool.md#the-tool)).

## Known issues

- **Set `KINDGI_SEED_USER_ID` in lower case.** With an upper-case UUID (macOS's `uuidgen` prints one), the seed user's grants show no projects, teams or reviewer role, an approval addressed to it can't be decided, and it can approve its own requests where a second person's approval is required. To tell: the people list shows the seed user in upper case, and project and team member lists show it in lower case. To fix it, set the value in lower case and restart the runtime. That makes a new seed user with the lower-case id, a tenant admin; the old one keeps its keys, so mint new ones for the new seed user ([People, API keys and service accounts](https://docs.kindgi.com/v0.3.1/deploy/people-and-keys.md)).

- **Set `KINDGI_TENANT_ID` in lower case** when you set up a new runtime. With an upper-case one (macOS's `uuidgen` prints one), parts of the runtime read the tenant as two, so some checks miss it: sign-in on a domain with its own identity provider, ending console sessions when the API token changes, and a retention purge or an erasure of files kept with a run. 0.3.0 fixes these checks. Until then, manage the tenant one way only. If its people sign in through its own identity provider, register the provider and manage the people with the API token as the bearer of your CLI or API calls, not in the console or with an API key. Otherwise use either way, but not both. Keep the id of a runtime that's already running as it is, even in upper case, including when you upgrade.

- **Use ids in lower case when you remove or change access through the API.** With an authorization store, removing or changing a project or team membership, a team's grant on a project, or a service account's grant with an id that isn't in lower case can leave the access in place, though the change shows as made. Use ids as the API returns them, in lower case. To take away access left this way, give it back as it was, with the ids in lower case, and then make the change again. Fixed in 0.3.0 ([People, API keys and service accounts](https://docs.kindgi.com/v0.3.1/deploy/people-and-keys.md)).

- **Set `KINDGI_PUBLIC_URL` on a server behind a proxy, in Docker Compose too.** A Compose file that serves the runtime on your domain through a proxy doesn't give the server its public address unless you add it. Without it, a paused tool call's link is made from the address the server binds, which the other system can't reach, so its answer doesn't arrive and the run waits until the wait times out. The server logs a warning the first time it makes such a link. Add `KINDGI_PUBLIC_URL: https://<your domain>` to the server's `environment` in the Compose file, or, on a runtime set up as [Self-host Kindgi](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/self-host.md) shows, `KINDGI_PUBLIC_URL=https://<your domain>` to its `kindgi.env`, and restart the server ([Wait for another system](https://docs.kindgi.com/v0.3.1/guides/tools/wait-for-another-system.md#on-a-server-you-run)).

- **Keep a paused call's link out of a pack tool's errors.** A failing pack tool's error is cleared of the call's secrets and connection tokens, but not of the link a pausing tool is given, so an error that repeats it keeps it where the run's errors are read. The link stops working when the call fails. Write such a tool's errors without the link. Fixed in 0.3.0 ([Wait for another system](https://docs.kindgi.com/v0.3.1/guides/tools/wait-for-another-system.md)).

- **Save a stored file rather than opening its download in a browser.** A browser that opens an artifact's download can show the stored file as a page of the runtime's own address, where the console is signed in. Download files with `kindgi artifacts download`, which saves each one to a file. If a proxy fronts the runtime, it can add `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff` and `Content-Security-Policy: default-src 'none'; sandbox` to the responses whose path starts with `/v1/artifacts/`. Fixed in 0.3.0 ([Keep files with a run](https://docs.kindgi.com/v0.3.1/guides/runs/keep-files-with-a-run.md#find-and-download-them)).

## Upgrade steps

- **From a 0.2.0 release candidate:** 0.2.0 is 0.2.0-rc.1's code. From rc.1, no migration applies, and only the version changes: its 90 days as a preview release count from 0.2.0's release. From rc.0, the first start applies the two of the seven migrations below that rc.1 added, which take a short lock on `kernel_runs`, `kernel_triggers`, `kernel_trigger_fires` and `service_accounts`, so back up first, as for any upgrade.

- **Log in to `registry.kindgi.com` before you pull 0.2.0,** on every self-host path: `docker login registry.kindgi.com --username kindgi` asks for your license key. The Cloud Run and Azure Container Apps pages copy the image into your own registry from there ([Pull the runtime image](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/pull-the-runtime-image.md)). `kindgi dev` pulls its runtime from there too: on each machine that runs it, `kindgi auth registry` logs in.

- **A small dev deployment on Cloud Run** (the module with `network_mode = "none"`, at 0.1.8 or later): run the module's `upgrade.sh` as of the `@kindgi/sdk@0.2.0` tag, since 0.1.10's copy goes to 0.1.10 by default, from the old registry. It copies the runtime from `registry.kindgi.com`, so log in there before you run it. Its plan also changes the pack's service in place, with or without `--pack-dir` (its CPU and its readiness checks, under Changed). For an earlier release than 0.2.0, use the script as of the `@kindgi/sdk@0.1.10` tag.

- **Set the public URL if it isn't set** (`KINDGI_PUBLIC_URL`; on Cloud Run and Azure Container Apps, `public_url`, empty by default), to the URL people open the console at: once it's set, the console's sessions are accepted from that origin only. A paused tool call's link is made from it. Without it, the link is made from the address the server is bound to, which another system can't reach, and the server logs that at the first pause. Connections stay off until it's set, and the server logs what else they need as it starts. Each app's sign-in sends people back to `<public URL>/console/connect/callback`.

- **Without an authorization store, check who changes the tenant's settings:** a script or a person that isn't a tenant admin there now gets `403 permission-denied` when it changes them, and on any call to the outgoing webhook endpoints or the compliance evidence. Use the deployment's API token or an `admin` key ([Security fixes](https://docs.kindgi.com/v0.3.1/releases/0.2.0.md#security-fixes) says who's a tenant admin there).

- **Seven new migrations, all additive:** new tables, and new columns that can be empty, with nothing rewritten, among them the one that keeps the waits of paused tool calls. The first start runs them in seconds, as it runs any other migration. Five of them take a short lock on `kernel_runs`, `kernel_triggers`, `kernel_trigger_fires`, `hitl_approvals`, `judgments` or `service_accounts`; the other two only add tables. 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.

- **A pack built for 0.1.10 keeps working without a rebuild.** To use 0.2.0's features in it, such as a tool that pauses, a connection or data blocks, move its pins to 0.2.0 first. A Python pack made by a 0.1 CLI pins `kindgi>=0.1,<0.2` and `kindgi-cli>=0.1,<0.2`: `uv add "kindgi==0.2.0"` and `uv add --dev "kindgi-cli==0.2.0"`. A TypeScript pack takes `pnpm add @kindgi/sdk@0.2.0` and `pnpm add -D @kindgi/cli@0.2.0`, and a Java or Scala pack `./kindgiw upgrade --to=0.2.0`. Upgrade the runtime before you deploy a pack with blocks: `kindgi deploy` refuses it on a runtime that doesn't publish them.

- **On Azure Container Apps, the plan refuses a `KINDGI_TENANCY` or `KINDGI_PACK_ROUTING` other than `single`** in `server_env`, and either one in `server_secret_env`: the module runs one tenant, and the server wouldn't start with them there.

- **With Cloud SQL by private IP, the Cloud Run module now gives the runtime the instance's CAs** (`KINDGI_DATABASE_SERVER_CA`), so from then on, rotate the server's CA in this order: add the new CA, apply, then rotate ([Rotate the server's CA](https://docs.kindgi.com/v0.3.1/deploy/self-hosted/database-tls.md#rotate-the-servers-ca)).
