Kindgi 0.3.0
Released October 30, 2026. For the docs of this version, see docs.kindgi.com/v0.3.0.
Security fixes
Section titled “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). -
A tenant id given in upper case is one tenant. Before this release,
KINDGI_TENANT_IDwas 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: withKINDGI_SEED_USER_IDkeeping 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 withKINDGI_AUTH_VERIFIED_DOMAINSnaming 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 asconnection 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).
-
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_IDwas 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.
-
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 --waitwaits 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=30mgives. 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 --waitwaits the same way for the versions a promotion carries (Release an agent version). -
Every pin that covers a project, in one read.
GET /v1/projects/<project-id>/pinned-versionslists 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 onnextCursor, not on the count. In the TypeScript client it'sprojects.pinnedVersions.list, and in the Python clientprojects.pinned_versions.list(Live versions reference). -
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 answers409 flow-version-pinnedor409 tool-version-pinned, naming the pins.kindgi promote --make-livealso pins each flow version it carries in the target project, and prints them under "Flows pinned in".kindgi exports createwrites each flow's and tool's pins and their changes (Pin a flow's version, 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}/versionspages a schedule's versions, newest first, and…/versions/{version}reads one (schedules.versionsandgetVersionin the TypeScript client,list_versionsandget_versionin Python), and the same for/v1/webhooksand/v1/mail-triggers. A trigger, a fire and a run'striggername theirversion. In the CLI,kindgi schedules,kindgi webhooksandkindgi mail-triggerstakeversions <id>andget <id> <version>, andlist --tablehas aVERSIONcolumn.kindgi exports createwrites 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). -
The Cloud Run module's
upgrade.shupgrades every network shape. It took a deployment withnetwork_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 anynetwork_mode, can run it, to 0.2.0 or later: use it as of the@kindgi/sdk@0.3.0tag (Cloud Run). From 0.2.0, a deployment whose database has a private IP can't upgrade to 0.3.0 with it yet (Known issues). -
An agent's model settings say how much its model thinks. A model-settings block takes
thinking, besidetemperatureandmaxOutputTokens:lowest(as little as the model allows),minimal,low,medium,high,xhighormax, and an improvement pass can try the levels its schema marks tunable. A provider's model lists the levels it takes in itsthinking'slevels, and adefaultfor 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 warnsthinking-level-adjusted. Each built-in adapter sends the level in its vendor's own field, and each model call's cost record says inthinkingthe 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 therekindgi providers register --presetregisters the models without them, and says so (Temperature and thinking). -
A guardrail can judge a tool call, its result or a model reply, not only the final answer. Its
atsays 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,haltfails the turn, andretryrefuses the call, withholds its result or sends the reply back, counting attempts across the turn. A check names where it can run withdefineCheck({ points }),scope.toolslimits a guardrail at a tool point to the tools it names, and a call abefore-toolguardrail judges waits for the calls before it. A check that can't run at its point fails ascheck-point-unsupported, orcheck-timed-outpast its time limit. Withsees: 'conversation', a check also gets the conversation's earlier turns (Where in the turn it runs). A guardrail a pack defines doesn't get all of this in this release (Known issues). -
GET /v1/runtimesays which runtime answers: its release inversion, such as0.3.0, whether it serves one tenant or many intenancy(singleormulti), and indevelopmentwhether it runs in development mode. Any of the tenant's tokens reads it, whatever its rights. It'sruntime.get()in the TypeScript client andruntime.getin Python (Restart and check health). -
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 instepUsage, 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 answers404 provenance-not-found, and says whether it's being written or failed (A flow's 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/permissionssays whether the license is out of your reach:licenseOperatorOnlyistruewhenGET /v1/licenserefuses 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. Thelicense-operator-onlyrefusals now say why in plain words, without a setting's name (People, API keys and service accounts).
-
A pnpm workspace pack's image no longer carries its dev dependencies: a pack with build steps (
prisma()'sprisma generate) installs its dev dependencies too, and prunes them after the steps. In a pnpm workspace, the prune left their folders innode_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 rannpx @kindgi/cli@0.1, whatever the release. Now they name that release's line, such as@kindgi/cli@0.2from 0.2.0. From a pre-release such as0.3.0-rc.0, npx finds no@kindgi/cli@0.3until 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 versionslists them) (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.shsays so as it starts the copy, and in--dry-run, and so do the deployment pages (Pull the runtime image). -
A tool's error says why it failed: a TypeScript tool whose
fetchfailed reported onlyfetch 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=orX-Amz-Signature=, become[redacted], and the tool's own secrets and connection tokens are taken out first. The same holds for a check'sevaluateand 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). -
A tenant id that isn't a uuid is refused with a message that names the setting.
KINDGI_TENANT_ID,--tenantor the config file'stenantmust be a uuid (Self-host 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 aseed_tenant_idorseed_user_idthat isn't a uuid at plan. -
kindgi artifacts downloadwrites 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--forcethe 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. Thesizeandhashit 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 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, orSIGTERM, 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 sayskindgi artifacts download stopped: no file was written.and exits with code 130. -
A write sent with an
Idempotency-Keythat 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 is503 idempotency-store-unavailablewithRetry-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 gets409 idempotency-key-in-flight, then runs again. Each such failure is now logged (Retry a start safely). -
The system API's tenant reads answer
503when the database is too busy.GET /v1/system/tenantsandGET /v1/system/tenants/{tenantId}, when the database's statement or lock timeout or the wait for one of the server's connections ended them, answered500. They now answer503(database-busy) withRetry-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-faileduntil the pack was rebuilt under a new tool version. A deploy now reinstates an unregistered tool version its pack brings, askindgi tools reinstatedoes, 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). -
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 thetool-,flow-,guardrail-andblock-ones, named a copy of the pack made withkindgi 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) andkindgi promotethe 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). -
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 --hostedandkindgi deployments getsay 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 printCouldn't read where each value is set:and why, such as the HTTP status and code. The deploy goes on, anddeployments get --jsonis unchanged. -
The
sampletemplate says it has three tools.kindgi init --template=samplemakesecho,greetandfetch-httpbin, andkindgi devloads all three, but the pack's README,package.jsonandkindgi.config.tssaid 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-scheduledentry saysleft: true, it runs again at the run's next resume and uses one of itsretry.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 withrun-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 withCONNECTION_DESTROYEDinstead 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_IDwith 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. WithKINDGI_ARTIFACTS=gcs:<bucket>/<prefix>, a purge lists, deletes and counts only the files under<prefix>/. Before, with the prefixprod, it also reached the same tenant's files underprod-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 lacksstorage.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_signalwas 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 readsmigration 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 whatkindgi service-accounts getshows. Each change now waits for the one before it, across all of the runtime's instances, andkindgi-runtime tenants repairwaits 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
Section titled “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).
-
Only agents and service accounts hold a run grant in the authorization model, apart from a site person's
calleron the site's project.invokerandexecutoron a project,calleron an agent,executoron a flow,invokeron a tool andexecutoron an eval suite admit no person or team now, andcalleron a project admits no team. People run agents, flows and tools as editors, as before (Authorization). 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 answers403and a tool call in a run is refused. Give that person or teameditoron 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 --checkexits 3 forgate-failedandneeds-approval.kindgi agents promoteexits 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-liveexits 3 when a version it carries isn't pinned (it was 0).kindgi eval-runs start --waitandkindgi eval-runs rescore --waitexit 3 when the run failed or was cancelled (it was 0).
-
kindgi auth loginkeeps another runtime's login. When~/.kindgi/config.jsonholds 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 setKINDGI_API_URLandKINDGI_API_TOKENto the new address and its key in the shell you use it from.--forcereplaces it, and a warning, under--quiettoo, 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--forcenow (Deploy your agent). -
A provider registration with an
adapter_configkey 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}/checklists the key in a newwarningsarray, andkindgi doctorshows the provider as a warning, with the steps to register it again. A custom adapter lists its keys in its entry'sconfigKeysto get the same; one that doesn't takes any key (A spec its adapter can't 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: falsethat 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 itsparallelToolCalls: false(parallel_tool_calls=Falsein Python) to run every call on its own, as before (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 --alltakes away what an older runtime gave that id (Who has access to a project). -
A framework skill is a folder, and the long ones load in parts.
kindgi skills sync,kindgi initandkindgi devcopy each skill'sSKILL.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 inSKILL.mdwhat'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). -
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,gcloudor 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 instancemust be replaced, stop, since a replace deletes the database. To delete the instance, setdatabase_deletion_protection = falseand apply first. An instance you serve from withdatabase_host_instanceisn't the module's: turn its protection on withgcloud sql instances patch <instance> --deletion-protectionbefore it serves. Use the module as of the@kindgi/sdk@0.3.0tag (Operate it). From 0.2.0, a deployment whose database has a private IP can't upgrade to 0.3.0 withupgrade.shyet (Known issues). -
The API's OpenAPI document lists
503 database-busyon every read. AGETorHEADwhose database is too busy already answered503withRetry-After, and the document now says so on each, butGET /healthandGET /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,Config1andConfig2(an MCP endpoint's config) areMCPEndpointConfigStdio,MCPEndpointConfigHttpSseandMCPEndpointConfigStreamableHttp, andConfig3toConfig5(a registration's) areRegisterMCPEndpointBodyConfigStdio,RegisterMCPEndpointBodyConfigHttpSseandRegisterMCPEndpointBodyConfigStreamableHttp.Content2andContent3areMCPPromptMessageContentTextandMCPPromptMessageContentImage,ResumeandResume1areCompleteApprovalResultResumeOkandCompleteApprovalResultResumeFailed, andBaselineandBaseline1areJudgedComparisonSummaryBaselineRecordedandJudgedComparisonSummaryBaselineVersion.Config6andConfig7areRegisterScheduleBodyConfigandPatchScheduleBodyConfig,Config8isRegisterWebhookTriggerBodyConfigandPatchWebhookTriggerBodyConfig, one for each body, andBaseline2isComparisonCaseResultBaseline. 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 doesbuild(), so an answer that carries one can still be copied and changed. Every enum class implementsOpenEnum(asString(),isKnown()andknownValues()). -
A
retryguardrail sends a failed answer back to the model. Before this release, a guardrail whose action isretrywas only recorded, aslog-onlyis, and the turn completed. Now a failedretryguardrail sends the answer back to the model, with the check's reason, for a new one.retry.maxAttemptscounts the answers it fails, the first one included (2 when left out). When they run out, the turn fails withguardrail-violation, and the error'sattemptssays how many answers the turn had. A failedhaltguardrail still fails the turn at once, and a guardrail's counts are now of answers judged (Send the answer back). -
A read whose database fails answers
503or500, not an empty list or404. 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 answer503 database-busywithRetry-Afterwhen the database is too busy for aGET, and500otherwise. A write that needed such a read answers500and writes nothing, where it answered400or404. A session that can't be looked up answers500(503on a busyGET), where a401signed the console out. A turn whose tools or guardrails can't be read fails, and a resume whose can't be read waits, asrun-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 repairalso 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-runshows 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). -
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. Underkindgi dev, update the CLI, or start with a fresh database (kindgi dev --reset). -
kindgi-runtime tenantscommands that change a database run only on one at their own version.purge,repurge,repair,make-admin,admin-key,give-capabilitiesandreset-second-factorexit2, 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'serasures replaybeforerepurge). Each holds the lock a starting server takes to migrate, so a server that starts while one runs waits for it (Serve many tenants). -
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
Section titled “Known issues”-
A guardrail of kind
llm-judgedefined in a pack never judges. The pack's index leaves outjudgeCapabilities, even when the guardrail sets it, so the runtime refuses that guardrail on every turn it applies to, deployed or inkindgi dev. With actionhalt, each of those turns fails as aguardrail-violationwhoseevaluationErrorsname the missingjudgeCapabilities. With any other action the turn emits aguardrail.errorevent and the reply goes out unjudged; that includesretryat the final answer, but aretryguardrail 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. -
In
kindgi dev, a pack guardrail'sat,seesandbudgetare ignored. A guardrail set to run before or after a tool call, or at a model step, runs on the final answer instead, so abefore-toolguardrail doesn't stop the call. Withsees: 'conversation'it judges the current turn only, and no time limit applies to its check, sincebudget.maxLatencyMsand its defaults hold only away from the final answer. Fixed in 0.3.1. -
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 emitsguardrail.error. With actionhaltthe turn fails. Withretryeach 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. -
On a deployed runtime, redeploying a pack in which a guardrail changed only its
atorseeskeeps the guardrail's oldatandsees. A deploy keeps an already-registered guardrail whose definition hasn't changed, and that comparison leaves outatandsees. 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 asactionorconfig, is refused asguardrail-already-registered. Fixed in 0.3.1. -
On Cloud Run,
upgrade.shwon't upgrade a 0.2.0 deployment whose database has a private IP. That's a deployment withdatabase_private_networkset anddatabase_deletion_protectiontrue, 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 inKINDGI_DATABASE_SERVER_CA. So the script's plan check stops withPLAN CHECK: the plan changes more than this upgrade does, namingtemplate.containers.env.KINDGI_DATABASE_SERVER_CAongoogle_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.
Upgrade steps
Section titled “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.shstops such a deployment before it applies anything (Known issues), and 0.3.1's takes it from 0.2.0 to 0.3.1 in one run (Kindgi 0.3.1). -
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,orgsandprojects; the sixth only adds a table. The one that adds trigger versions keepskernel_runs,kernel_triggersandkernel_trigger_fireslocked 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_IDmust 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-idor the config file'sseedUserId. Outsidekindgi dev, such a runtime used to start. Set it to a uuid before you upgrade. An upper-case uuid (macOS'suuidgenprints 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). -
A
KINDGI_TENANT_IDwith 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. KeepKINDGI_TENANT_IDas it is until they're done, and register a new identity provider only once every replica runs 0.3.0:- With
KINDGI_ARTIFACTSset, move the stored files. In agcs:bucket:In aTerminal window 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>local:directory on a disk that tells case apart, as on Linux: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.Terminal window rsync -a <dir>/tenants/<id as set>/ <dir>/tenants/<id in lower case>/rm -r <dir>/tenants/<id as set> - With authorization on (
KINDGI_OPENFGA_API_URLset), once every replica runs 0.3.0, runkindgi-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. - Set
KINDGI_TENANT_IDin lower case, and restart.
- With
-
A Gemini registration that set
adapter_config.thinkingLevelsstops using it. After the upgrade the key isn't read, so its models think as Google sets by default, andkindgi doctorshows the provider as a warning. Move eachmodel=levelpair into that model'sthinkingas itsdefault, then unregister the provider and register it again from its spec (A registration that setthinkingLevels). In code,GeminiProviderOptions.thinkingLevelsand theGeminiThinkingLeveltype are gone: set each model'sthinking.defaultinstead. -
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
needsApprovalWhenUnattendedor 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, whosecreatedByis 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 createorgrantnaming a project by an id with an upper-case letter, such as one fromuuidgenon 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:- Remove each person the account added to the project, then add them again if they should stay.
- Remove each team grant the account made on the project, then grant the team again if it should stay.
- 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.