# Changelog All notable changes to Druks. Versions follow [semantic versioning](https://semver.org); while Druks is pre-1.0, a minor bump may break compatibility. ## [0.13.0] β€” 2026-11-05 ### Breaking changes - **An agent call reads, then acts on the box.** `Workspace` is a plain class that Druks builds as `workspace_class(subject)`: `setup()` reads rows before the box exists, `prepare()` acts on the box with no session bound, and Druks sets `host` in between. `Workflow.get_workspace_kwargs()`, `get_workspace()`, `Workspace.prepare_context()`, and dataclass workspaces are gone. `Host.save_files()` replaces `Workspace.save_files()`, `File.upload()` ships a file argument, and `set_git_identity()` takes no session. A read in `prepare()` or in a hook raises `SessionNotBoundError` instead of holding a connection through the run. - **A `RepoWorkspace` subject names its `repo` and `ref`.** `ref` is the commit or branch to clone, `None` for the default branch, and a subject without it raises. `RepoWorkspace.branch` and `get_repo()` are gone. - **A managed service's OAuth redirect is `/services//oauth/callback`.** The consent URL and the token exchange both carry it, and `Service.get_oauth_client()` derives it, so apps change nothing. Deploy only after the manager accepts the path. Self-hosted OAuth keeps `/api/oauth/callback`. - **`Gate.answer()` requires `workflow=`.** Two workflow kinds on one subject can wait on the same gate name, and the answer now reaches the kind it names. - **Short-lived tokens live in the vault, not in Redis.** Resource tokens, GitHub installation tokens, and OAuth access tokens are encrypted rows with an expiry. `GitHubAppClient` signs with the App key and `GitHubClient(token)` makes the installation calls; `GithubApp.get_client(owner_or_repo)` and `get_app_client()` build them. `get_mention_handle` is gone, and `evict_access_token` takes the session. - **Druks is licensed under FSL-1.1-ALv2.** Self-hosting, changes, apps, and professional services stay free; a competing hosted service is not allowed. Each release becomes Apache 2.0 two years after it ships. `docs/licensing.md` has the details. ### Added - **An app has a directory it may write.** `App.get_data_dir()` returns `/apps/` and creates it. A downloaded tool or a tool's state goes there, never under `~`. - **A workflow can wait on an event without parking.** `self.wait(topic, label=, ttl_seconds=)` suspends the run while it reads as running, and `Workflow.answer(subject, topic, **facts)` answers it from a subscriber. A wait past its TTL fails the run with `WaitTimeout`, and `answer(subject, topic, failure=...)` fails it with `WaitFailed`. - **App code gets its account.** `druks.accounts` exports `Account`, `AccountKind`, and `current_account`. A route takes the caller's row with `Depends(current_account)`, and a run carries `self.account`. - **Jira events reach every app.** The Jira webhook moves into core and turns a Jira Automation issue into `ticket.transitioned`, with no Software Factory needed. The URL stays `/_external/jira/events/`. - **Linear and GitHub issues publish `ticket.created` and `ticket.updated`.** Every `ticket.*` event carries one set of keys whatever the tracker: `source`, `ticket_id`, `identifier`, `title`, `url`, `project_name`, `labels`, and `actor_id`. Linear events add `team`. - **GitHub issues publish `issue.opened`, `issue.edited`, and `issue.reopened`**, with `repo` and `number` beside the payload. - **`druks doctor` names runs parked past the gate timeout**, in `stale_parked_runs`, so an operator can cancel them. - **The sandbox image has `jq` and `file`.** - **Python 3.15.** ### Changed - **A workflow sandbox starts with a thirty-minute lease**, and an active workspace renews it every five minutes. An idle or dead worker lets its box expire. - **A recovered agent call attaches to the agent the stopped process left running**, instead of starting a second one beside it. - **The host takes the harness and the manifest, not a session**, so an agent run holds no database connection. - **Software Factory** reviews a build's pull request with the standalone review and waits on it: `request_changes` returns to implement, up to `max_implementation_revisions`, and `approve` parks for review. The evaluator, `review_code`, and `journal.evaluations` are gone. A cancel or a close without merge returns the ticket to rest while it is in progress or in review, and a tracker reads a status name with `get_status_name`. A build finishes when GitHub merges its pull request, fails before review when the planner cannot plan, answers 409 with the owning project when a repository is already registered, and caps a ticket title at 200 characters on edit as on create. - **The MCP server names its website URL**, and `chat_require_tag` describes its argument. ### Fixed - **`Workflow.cancel()` and `answer()` bring their own session**, as `start()` does, so a workflow body can cancel a subject's run and start another. - **Druks installs beside githubkit 0.17.** hishel is bound below 2, which removed the transport githubkit imports. - **After-agent hooks run on boxes without Node**, and a run whose hook never ran fails instead of finishing. On Python 3.11 a hook's agent call runs in the step's own task, so it no longer raises `SessionNotBoundError`. - **`druks check-app` accepts an app that imports `druks.services` first.** The notifications package init no longer imports the outbox. - **DBOS logs no duplicate traceback** for a `FatalError` or `HarnessError` that Druks already records. - **A call with an unusable subscription token is refused** before a box is provisioned. - **A run waiting for Claude's usage window shows as running.** ## [0.12.0] β€” 2026-10-31 ### Breaking changes - **A service declares how it authenticates in `Authentication`.** This nested class names its ways with the kinds in `druks.services.auth`: `ApiKey`, `Jwt`, `Oauth`, and `SignIn`. The OAuth endpoints and options move there from the `Service` class. `api_key`, `private_key`, `client_id`, and `client_secret` leave `Settings`. `get_authorization`, `get_identity`, `get_consent_query`, `read_grant`, `is_grant_revoked`, `get_install_endpoint`, and `issue_token` move to `Authentication`. - **A sandbox secret names a tuple of resources.** `SandboxSecret.resources` and `SandboxMcpServer.resources` replace `resource`. `GitHubClient.get_token_for_repos(*repos)` replaces `token_for_repo(repo)`. A sandbox's GitHub token reaches only the repositories that its workspace names. - **A notification destination is a Slack channel.** The kind `slack_channel` and `channelId` replace `slack_webhook`, `url`, and `hasUrl`. Druks posts as the bot of the Slack card. A migration deletes each `slack_webhook` destination and its notifications, and clears the defaults that name one. Apprise is gone. - **`.druks/software_factory/review.md` replaces `.druks/review/checklist.md`.** - **`pr.opened` is a platform fact from GitHub's webhook**, with `repo`, `pr_number`, and `payload`. Software Factory records `build.pr_opened`. ### Added - **An app can give an agent one more turn.** An `after_agent` hook runs when the agent ends its turn. If it returns text, the agent gets the text as one more turn. A hook can sit on the app's own agent, or on another app's agent through `Agent.get(id)`, so neither app imports the other. Druks discovers `hooks.py`, and `RepoWorkspace.get_diff()` gives a hook the agent's change. - **A workflow can run a command in a VM.** `Workflow.exec(command, timeout=...)` runs it in a VM of the workspace and returns a `subprocess.CompletedProcess`. - **An app can act on pull requests.** The GitHub webhook publishes `pr.opened`, `pr.synchronized`, `pr.edited`, and `pr.ready_for_review`. `GitHubClient.post_check_run()` posts a check run with annotations. - **An app can cache a provider read.** `App.get_cached()`, `set_cached()`, and `clear_cached()` keep JSON values in Redis, and each value expires. - **`druks.toml` can switch off an app or a service.** Set `enabled = false` in `[apps.]` or `[services.]`. `druks setup` keeps both tables. - **Druks can act as an installed Linear app.** The Linear card takes an install or a pasted API key. Druks then uses that credential for tickets and for Linear's MCP server. - **A manager can own the Slack and Linear apps.** The card then shows **Install**, and Druks uses the manager's app for messages, button clicks, and account links. - **Choose the Claude sign-in method.** `sign_in` in `[providers.anthropic]` is `oauth`, the default, or `setup_token`, a pasted `claude setup-token`. - **A sandbox can load the apps of a subscription.** The **Include apps** setting is off by default. When it is on, a run loads the subscription's claude.ai connectors or ChatGPT apps. - **The Slack bot introduces itself.** It posts a welcome when it joins a room, and before the first link that it sends to a person. - **The agent can read files on a thread.** `chat_read_thread` lists the files on each Slack message, and `chat_read_file` puts one in the sandbox. - **Chat reacts with πŸ‘€** to each message from Slack, GitHub, or WhatsApp when its delivery starts. - **An MCP server can pin the scopes that a connect requests.** The **Scopes** button on the server's row shows what the server advertises. - **The effort levels `xhigh` and `max`.** Codex runs `max` as `xhigh`. - **A theme preference.** Each account chooses System, Light, or Dark under Settings β†’ Preferences. - **`ResourceError` keeps `status_code` and `reason`**, so an app can tell a refusal from an outage. ### Changed - **A sandbox secret that needs a person fails the run at once.** The failure gives the reason, such as a GitHub App that is not on the repository. Druks needs a current Drukbox for this. - **Every MCP tool failure is one JSON object:** `{"code", "message", "retryable"}`. A wrong call is `INVALID_TOOL_ARGUMENTS`. - **Claude loads every Druks tool upfront.** The Chat agent searches for a connected service's tools before it says that it cannot reach that service. - **Chat asks the agent once for a missing channel reply.** The person gets the failure line only when the second turn also ends with no text. - **A new web conversation gets its title during the first reply.** - **Activity shows every finish.** A routine start records no row. - **A `[services.]` table that no installed app declares is logged and skipped.** Druks does not refuse to start. - **The GitHub and Slack manifests ask for more.** GitHub adds Checks write and Repository creation write, and Administration drops to read. `druks doctor` names each permission that the App lacks. Slack adds `reactions:write`, `channels:read`, `groups:read`, and the Interactivity URL. - **The Linear webhook is in core**, so a standalone app can read Linear facts without Software Factory. The endpoint does not change. - **A Software Factory build creates its branch on GitHub first.** A branch name that exists gets the next number. A pull request from a fork never fills a work item. - **A Linear ticket routes to its repository by its labels**, then by its project name. - **Agent commits and pull request titles follow the repository's format**, when its `AGENTS.md` or contributing guide gives one. - **A person's Linear sign-in asks for `read` and `write`.** - **The Schedules filter leaves out builtin apps**, as the Dashboard does. ### Fixed - **The image no longer trusts the `emSign ECC Root CA - G3` root.** Both certifi and `ca-certificates` drop it. - **A Codex run waits for its MCP servers before its first turn.** A slow server was missing from the run. - **A CI check that a path filter skips does not block an evaluation.** - **The dispatcher's co-author line stays** when a commit has other co-authors. - **The web manifest loads behind an access check that needs a cookie.** - **The Atlassian directory entry uses `/v2/mcp`.** A connect to the old endpoint requested no scope. ## [0.11.0] β€” 2026-10-18 ### Added - **An app can keep resources at a provider**, such as a Cloudflare Worker, and get a token for each one. Declare a `Resource` class inside the service class, as you declare `Settings`. It has four steps: `create`, `update`, `delete`, and `get_token`. The app makes every other call with `get_token()` and the provider's SDK. When a manager manages the service card, Druks sends the four steps to the manager, and the app code does not change. A failed step raises `ResourceError`. `resource.get_secrets()` gives a sandbox the resource's token as a placeholder, such as `CLOUDFLARE_API_TOKEN`. `Resource` and `ResourceError` come from `druks.services`. ## [0.10.0] β€” 2026-10-18 ### Breaking changes - **A Bot's number is public or private.** `BotAccess.PUBLIC` replaces `BotAccess.OPEN`, and `BotAccess.PRIVATE` replaces `BotAccess.PAIRED`. On the wire, `botAccess` is `public` or `private`. ### Added - **Reminders in chat.** Ask the agent to remind you or another person, one time or on a schedule. Druks sends the reminder word for word through the conversation's channel: web Chat, Slack, or WhatsApp. A reminder takes a delay, a time, or a cron in the installation timezone, and the agent gets the time on each turn. The agent tools are `chat_create_reminder`, `chat_list_reminders`, and `chat_cancel_reminder`. The Chat page shows each reminder. - **WhatsApp groups.** A public number answers in a group after its admin tags it there. Then a message that tags the number or replies to it reaches the Bot. On a private number, only the tag of a paired phone reaches its agent. The agent reads the group with `chat_read_thread` before it answers, and tags a member with `@` and their number. A number linked before this release ignores groups until you link it again. - **More for WhatsApp.** On a private number, an operator's agent can start a chat with a phone number through `chat_start_conversation`, for example to set a reminder there. A shared contact card reaches the agent as text. - **GitHub Issues as a Software Factory tracker.** An issue that gets the trigger label opens a build. The five status settings are label names, and the done label closes the issue. A ticket key is `owner/repo#number`. The operator App must subscribe to the `issues` event. - **An agent can drive a saved browser session.** `Agent(browser=session, browser_client=BrowserClient.PINCHTAB)` gives the call five browser tools through the Druks MCP server. The sandbox never holds the browser's endpoint, token, or profile. In app code, `BrowserSession.pinchtab()` borrows the session, and `BrowserSession(read_only=True)` lets only GET requests through. - **A run waits for a sandbox slot.** When Drukbox is at capacity, the run waits, and the dashboard shows "Waiting for a sandbox slot". `[sandbox.host_limits]` in `druks.toml` sets the limit of each provider. - **A default for Slack tags.** On the Slack pane of Chat β†’ Channels, a person sets whether the bot answers only their tagged messages in new Slack threads. - **A Jira service account can connect.** Druks calls Jira through the Atlassian gateway, so a scoped service-account token works. - **A tool can depend on configuration.** A router dependency that raises `ToolNotAvailable` hides the router's tools from `/mcp` until its condition holds. ### Changed - **A chat hears every run it started**, also a run that returns nothing. That report names the run's subject, so the agent can read the output there. - **A chat channel gets only the reply after the agent's last tool call.** The notes between tool calls stay on the Chat page. - **A chat turn with no connected provider fails at once.** On a channel, an operator's conversation gets the cause, such as "connect your Anthropic subscription". A Bot's conversation keeps the fixed reply. - **The Chat agent answers with outcomes.** It keeps Slack and GitHub replies short, loads a tool's schema before its first call, and ends its turn after it starts a run. In a thread that other people read, such as a WhatsApp group, it does not list a person's connections unless that person asks. - **An MCP tool call with an undeclared argument fails**, and the error names the argument. - **The board ticket tools show only while Software Factory's tracker is `druks`.** - **`software_factory_start` answers a tracker refusal with an agent error:** 409 `TICKET_NOT_MOVABLE` or 502 `TRACKER_REFUSED`. - **`get_gate` reads a gate that is answered on its source.** `answer_gate` still refuses it. - **`druks sandboxes build` prints the progress of each template build.** ### Fixed - **The browser template builds on any docker base image.** It failed on every Compose install. - **A live chat session gets new instructions and MCP servers on its next turn.** It kept its first prompt and servers after a deploy, or after an operator connected a server. The session keeps its history. - **A per-person OAuth MCP server does not break other accounts' runs.** A run whose account has not connected the server leaves it out. - **A push that changes a `.druks` file reaches the next build.** Druks used its cached copy for 24 hours. - **A build does not wait on GitHub's anonymous rate limit.** The App's git author lookup uses an installation token. - **A directory upload to a sandbox does not hang** when tar writes many warnings or the stream stops. - **A skill repository URL without `https://` installs.** For a private repository without the App, the error names that cause. - **A long URL in a card description wraps.** ## [0.9.0] β€” 2026-10-12 ### Breaking changes - **`druks.toml` holds no secret.** Each secret comes from its environment variable, such as `DRUKS_SECRETS_KEY`, or from a file in `DRUKS_SECRETS_DIR`. The `[secrets]` table is gone, and Druks does not start when `druks.toml` holds a secret. `druks setup` moves each secret to the last section of `.env`. - **`druks sandboxes build` builds the sandbox templates.** It waits for each build and fails with its error. `druks doctor` only reports the templates. Run the command after each deploy. Druks needs a current Drukbox. - **The browser is a sandbox template**, like Chat. The browser image and the settings `sandbox.browser_*` are gone. A `[browser]` table holds `proxy_scope` and `timezone`, and `DRUKS_BROWSER_PROXY` names the proxy. - **Registry settings live in `[sandbox]`:** `registry_host`, `registry_username`, `registry_password`, and `template_repository`. The `EXE_*` registry keys in `[sandbox.exe]` are gone. - **Druks runs on DBOS 3.** Do not run a 0.8 process and a 0.9 process against one database at the same time. - **`GITHUB_API_URL` is gone.** Every GitHub API call goes to `https://api.github.com`. - **A directory of MCP servers replaces the registry search.** The dashboard offers a vetted list of hosted servers that sign in with OAuth. `DRUKS_MCP_DIRECTORY` replaces `DRUKS_MCP_TRUSTED`. Add a server that is not in the list as a custom server. - **Software Factory does not merge on approval by default.** `on_approval` is `none`: Druks marks the pull request ready for review and ends the build. Set `on_approval: merge` in `.druks/software_factory/config.yml` to merge. - **WhatsApp has one link.** Add a number, then pair a phone with the code. The separate path for your own number is gone. A code that the linked phone sends to itself makes that chat yours. - **A sandbox setup script sits beside its module.** The path is relative to the folder of the module that declares the `Sandbox`, not to the app package. `Sandbox.package` is gone. ### Added - **Phone calls.** Link a Twilio number to an app's Bot, and the Bot answers calls with a speech-to-speech model from OpenAI or Google. The Bot's tools work during the call, and the caller's earlier calls are context. Any operator reads a number's calls in the app's Channels tab. The calls server is a separate program with its own image, `ghcr.io/czpython/druks-calls`, and the default stack does not run it. `docs/deployment.md` describes how to add it. - **The operator's Chat reaches more.** It reaches every enabled MCP server. Its sandbox holds the operator's own sign-in at each service with a `host`, so `git` and `gh` act as that person. The prompt lists the sign-ins it holds. A Bot still reaches only the Druks server. - **An agent declares the service secrets its sandbox holds.** `Agent(secrets=(Acme.fields.api_key,))` gives the sandbox `ACME_API_KEY`. `Service.host` is the one host that the secret can go to. - **An MCP server can use its service's credential.** `Service.mcp_host` names the MCP host that the service's identity signs in to. GitHub, Jira, and Linear set it. An account with a sign-in at the service needs no second consent. Other accounts of a pasted-login service connect their own grant, and the default account sends the login that `Service.get_authorization()` builds. - **A header MCP server can hold one key for each person.** Each person sets their own key on the server's row. - **Disconnect a service** in Settings β†’ Services. Druks revokes its credentials and every account's sign-in to it. - **Create the reviewer GitHub App from Settings**, like the operator App. Each service that declares a `manifest` gets **Create App**. - **Managed services.** A `[services.]` table in `druks.toml` holds the plain fields of a service card, and its secrets are files in `DRUKS_SECRETS_DIR`. Druks syncs the card at startup and refuses edits to it. With a `[manager]` table, a manager can answer a service's API calls and sign its webhooks. The card then differs only in its `url` and its key. - **A person can require a tag in a Slack thread.** The agent tool `chat_require_tag` turns off the untagged replies for that conversation. - **The browser proxy can cover every launch.** `browser.proxy_scope` is `login`, the default, for the login window only, or `all` for every borrow too. - **More page and service blocks.** An app with no landing page gets a platform home: one live table for each subject type. `Cards(layout="tiles")` crops each image to a square. `Columns(layout="split")` is a page of two panes: a list and what it opened. A service card renders its `description` as Markdown, and a field with a default can stay empty. - **More author reads.** `Service.list_for_account(account_id)` reads an account's sign-ins without a scope declaration. `GitHubClient.create_reaction()` reacts to a comment. - **Instances can share one host's Postgres and Redis.** `DRUKS_WEB_PORT` sets the web port. `DRUKS_DATABASE_POOL_SIZE`, `DRUKS_DATABASE_MAX_OVERFLOW`, and `DRUKS_DBOS_POOL_SIZE` set the pool sizes. Druks supports Postgres 16 and newer. ### Changed - **A chat hears every run it started.** A run reports its result when it waited or returned something, and it always reports a failure. Web Chat hears its runs too. - **A failed Chat delivery retries** up to five times, with a backoff. Then its messages end as `failed`. The page shows **Send again** on a failed, interrupted, or cancelled message. A Slack, GitHub, or WhatsApp conversation gets one short line that the message failed. - **The login window runs Google Chrome as a plain process.** Playwright attaches only for a short time to add and read cookies, so a site sees no debugger. - **OpenAI subscriptions connect with Codex device authorization.** Druks shows a code and a link. First, enable device code authorization for Codex in ChatGPT. - **An agent call can run for 2 hours.** The Agents settings offer 7200 seconds. - **The Dashboard shows your runs and the unattended runs**, not the runs of other operators. - **A tracker build runs under the person who moved the ticket**, then the assignee, then the default account. A Jira rule sends `{{initiator.accountId}}` in the `x-jira-initiator` header. - **An unlinked person in a Slack room gets the link in a direct message**, and the thread says so. - **Claude Chat sessions have auto memory off.** Subject pages start with every transcript collapsed. ### Fixed - **Only the owner of a connection can reconsent or revoke it.** A reconsent wrote the caller's grant into another person's connection, and Settings listed the connections of every account. - **Only a reviewer who can write to the repository answers a build's review gate.** On a public repository, any account's approval could answer it. - **The Chat agent reaches the Druks tools.** The bridge sent `Bearer Bearer`, and Druks refused it. - **A Chat message is sent again when the bridge never got its prompt.** A turn never runs twice. - **A new key, or a switch between harnesses on one key, gets a new sandbox.** A warm sandbox kept the old key entries, so Codex failed without `CODEX_API_KEY`. - **A scheduled run gets its sandbox.** Drukbox refused its run id as an idempotency key. - **A catalog MCP server with a static key can take its key.** Nothing could store it, so every run failed while the server was enabled. - **`druks init-db` works from a wheel install.** The package carries `alembic.ini` and the core migrations. The developer command is `uv run alembic -c backend/druks/alembic.ini`. - **WebSockets work behind an edge that rewrites `Host`.** Druks compares `Origin` with the host of `urls.endpoint`. - **Chat and the login window work on a docker-sbx host.** - **A link to a subject whose id holds `/` or `#` opens that subject.** ## [0.8.0] β€” 2026-09-27 ### Breaking changes - **App tables and subjects.** `druks.db.Model` replaces `druks.db.Base`. A `StoredSubject` adds the columns `created_at` and `updated_at`, so each app needs a new revision. `__str__` names a subject and replaces `get_key()`. `SubjectSummary` has no `title`, and runs, events, and the Activity feed record only the name. An identity-only `Subject` overrides `get_or_none(id)` in place of `get_for_subject_id()`. - **Renamed author names.** `druks.db.fields` replaces `druks.secrets.fields`. `Webhook.get_delivery_key()` replaces `delivery_key()`. `Service.scopes()` replaces `required_scopes()`. `Workspace.get_secrets()` replaces `get_secret_refs()` and returns `SandboxSecret`. `Workspace.get_mcp_servers()` replaces `get_required_mcp_servers()` and returns `SandboxMcpServer`. Both types come from `druks.sandbox`. `Workspace.get_all_mcp_servers()` replaces `get_mcp_delivery()`. - **Pages and routes.** `ui.GateControls(subject)` takes the subject, not a run id. `Table(empty=EmptyState(...))` replaces `Table(empty_text=...)`. Druks adds the app name to every app operation id, so an app writes the bare verb. An id that starts with the app name fails at boot. - **A GitHub tag goes to Chat** and does not start a review by itself. The GitHub card needs the App's client ID and client secret, and the App needs the callback URL `/api/oauth/callback`. The fact `issue.commented` replaces `pr.commented`. - **`SLACK_SIGNING_SECRET` is gone.** The signing secret on the Slack card checks the buttons in gate notifications. ### Added - **Slack is a Chat source.** Write to the Druks bot in a direct message, or tag it in a room, and your own agent answers there, under your account and with your tools. In a room, the agent answers in the thread of the tag. For 24 hours after the last message, your untagged replies in that thread reach it too. A file you send becomes a Druks file on your message. Connect the Slack card under Connections β†’ Services. **Create Slack App** opens Slack with the Druks manifest filled in. Each person links their Slack account under Chat β†’ Channels. The guide is `docs/chat.md`. - **GitHub is a Chat source.** Tag the operator GitHub App in an issue or pull request comment, and your own agent answers with a comment where the tag was. The agent decides what to do: it answers, opens a ticket, starts a build, or starts a review. Only a person who can write to the repository reaches an agent. Each person links their GitHub account under Chat β†’ Channels, and gets their own conversation for each issue or pull request. In Slack and GitHub, the agent reads the thread with the `chat_read_thread` tool. - **Chat runs on Codex and OpenCode** as well as Claude. Select the harness in Chat's row under Chat β†’ Channels β†’ Bots. A conversation that moves to another harness starts a new agent session and keeps its messages. OpenCode ignores the effort setting. - **The chat agent gets the files people send** on WhatsApp and Slack. An image reaches the agent with its message. Any other file, except audio, reaches it as a link to a copy in the sandbox. A Bot's agent has no file tools, so it sees only the link and the file name. A voice note becomes text before its turn, through the new **Speech To Text** card. The card takes any server that speaks the OpenAI audio API, such as OpenAI, Groq, or a local server. Without the card, or for a note over 25 MiB, the agent asks the person to write instead. - **App tables have a `Model` base.** It names the table `_`, and has `create()`, `save()`, `delete()`, and the reads `get()`, `get_or_none()`, `all()`, and `filter()` by field. `get()` raises `ObjectNotFound` from `druks.exceptions`. A route answers it with 404, and a page shows an empty state with a link back. A class can declare its order: `class Report(Model, ordering=("-created_at",))`. A `StrEnum` or `Literal` column is text with a CHECK of its values. A `list` or `dict` column is JSONB. `druks makemigrations` names an app's revisions `_0001`, `_0002`, and so on. - **A `StoredSubject` needs less code.** Its `__tablename__` is optional, and its board shows its newest hundred rows, so `list_summaries()` is optional. Its default name is its type and id, such as `note 7`. - **Apps need less code in other places.** `ui.SubjectStatus(subject, working=...)` shows where the work on a subject stands. The shell links each page that follows one subject to that subject's timeline. `ScopedService.connect_url` is where an operator connects the service. `Agent(include_mcp=False)` gives the call no MCP server. `Workspace.get_env()` adds environment variables to every agent call. A service overrides `get_consent_query()` and `read_grant()` for a provider with a different OAuth shape. `druks.services` exports `Connection`, and `druks.workflows` exports `GateTimeout`. - **`druks check-app `** loads one installed app and checks its subjects, pages, operations, and routers. It needs no database. - **A custom MCP server can authenticate with a secret header**, such as `x-api-key`. The form offers Bearer token or Header. A migration moves each stored bearer token to the same header storage. ### Changed - **A failed run tells its conversation**, on every channel, also when the run did not wait. The agent says so in one short line, without error details, and does not retry. - **A service card keeps what you do not enter again.** On a connected card, a blank secret keeps the stored one. A new paste revokes the linked accounts only when the client ID changes. The error names each missing field. ### Fixed - **`druks.workspaces` and `druks.sandbox.client` import on their own.** As the first import in a process, each failed with a circular import. ## [0.7.0] β€” 2026-09-23 ### Added - **Chat.** Talk to an agent that acts on Druks as your account. A conversation is a live agent session in a sandbox that your account leases, not a workflow. Send stays available during a reply, with Stop beside it, and a queued message waits its turn. After a restart, Druks runs an interrupted delivery again. The list has title search, pins that stay with your account, and day groups in your timezone. Chat runs on Claude in this release. Its harness, model, billing, and effort come from its row in Chat β†’ Bots. The guide is `docs/chat.md`. - **WhatsApp is a Chat source.** Connect a WAHA service under Connections β†’ Services, then link a number from an app's Channels tab with a QR code. A person who writes to the number gets their own conversation, and Druks sends the reply through WAHA. The person who holds the number's phone is its admin. A run that waits asks that admin in the phone's chat with itself, and only the admin can answer. A message typed on the phone pauses the chat for four hours. An operator links their own number under Connections β†’ Accounts. WAHA must run the NOWEB or GOWS engine and reach `/_external/waha/events/`. Set `urls.webhook_host` or `urls.endpoint` before you link a number. - **An app can declare a Bot.** `Bot(prompt, user_tools, admin_tools)` from `druks.agents` answers the numbers linked to the app. A route tagged `bot` is a tool of the Bot, and it takes the person who writes as `user: BotUser`. A run that a bot tool starts remembers its conversation, so `self.review()` asks the number's admin. The Bot has a row in the app's Settings β†’ Bots as `.bot`. - **A `YesNo` gate.** `druks.workflows.YesNo` parks with the actions `yes` and `no` and an optional `note`. A gate's own fields shape its review: the note box shows only when the gate declares `note`, and the values of `action` are the controls when the ask names none. The review shows the question in the ask's `label`. - **A page can put actions on more blocks.** A `Callout` takes `controls`, a `Form` takes `extra_actions`, a `Table` takes `select` with row `actions`, a `StatusValue` takes a `link`, and a `ControlsValue` puts actions and links in a cell, a fact, or a list item. An operation that answers `{"url": "https://..."}` sends the browser to that URL. - **A skill collection installs from a private repository** when the GitHub App is installed on it. ### Changed - **Activity is compact and app-scoped.** Rows take two lines under day headings, and a panel shows the details. Filters and the date range live in the URL. A type belongs to its app. Search matches the recorded work key or title. Rows show work titles, PR references, review summaries, and failure causes. A spent budget records as `spend_limit`, and the run fails without a quota wait. The live stream resumes from a transaction snapshot, so a late commit does not disappear. - **A subject's stable handle is `key`.** `get_key()`, `subject.key`, `SubjectSummary.key`, and `subjectKey` on the wire replace the label. A migration renames the column. Rename an override of `get_label()` to `get_key()`. `get_summary()` projects the id and the key; override it for a title. An announcement that names `title`, `run`, or `kind` raises `WorkflowError`. - **A `Choices` source returns dicts.** Each choice is `{"value", "label"}` with an optional `group`. This replaces `(value, label)` pairs. The select is searchable and grouped. A stored value that the source does not list shows under Unavailable choices, and save and `druks doctor` reject it. - **Schedules is a full-width page.** Rows group by app. Preset and pause changes save at once. Each row shows a next-run estimate and up to eight recorded invocations. Run now queues one invocation, and a paused schedule stays paused. - **Software Factory pages are easier to scan.** Projects has collapsible headings, flat repository rows, and an inline repository profile. Overview has compact desktop rows. A retry shows as a continuation of its build, with the summed time and cost, and Retry works only on the latest run of a work item. The work item shows a finished call's artifact as a tab beside the transcript, and the evaluation is that artifact. An external review request shows as information with a link to its source. - **The evaluator is quieter.** It files no follow-up tickets. Its findings stay in `review_notes` and in the GitHub review, and the operator decides what becomes work. After the first round, a review says only what changed. A rework push resolves the review threads it addressed. A review request keeps the note around the mention. A retry with no failed step forks from the last declared step. Jira gets no transition when the status already matches. - **The GitHub App needs Administration: write** to create a repository from a template. An existing App must add the permission and accept it on the installation. The manifest routes now serve under `/api/core/services/github/`. - **`urls.webhook_host` is the HTTPS host of this installation's `/mcp` endpoint.** When it is empty, `urls.endpoint` is the base. `docs/deployment.md` describes an installation behind an external proxy. - **Connections explain more.** Removing an MCP server lists the accounts whose connections it revokes. MCP sign-in shows "Preparing sign-in" and gives discovery and client registration 30 seconds in total. A failed identity lookup records its reason on the connection. - **Codex reserve quota is separate from normal capacity.** An empty reserve raises no exhaustion alert while the main quota has capacity. The Usage page keeps the reserve collapsed until the main quota is exhausted. Codex model variants are priced by their family, so a dated `gpt-5-mini` id gets the mini rates. - **A lock that a dead process holds frees in 30 seconds.** Provider and OAuth token refresh, the sandbox rotation gate, and the browser writer lock renew one TTL. A run that loses its lock fails with `LockLostError`. Reading a saved browser session's status no longer decrypts its profile. - **The sandbox and browser images boot with the Drukbox base entrypoint.** The SSH key, the caller environment, the secrets-proxy CA, and the `gh` git credential come from the base. Both images have `build-essential`. The browser image installs Google Chrome on arm64 too. - **`druks create app` writes hyphenated folder and distribution names.** The app name and its package keep underscores. The command accepts both spellings. - **The execution-defaults error names the agent** that a default change breaks. ### Fixed - **Druks resumes every run after an upgrade.** A run parked at a gate before an upgrade stayed pending, because any edit to any workflow changed the DBOS application version. The version is now one constant, so DBOS resumes parked and interrupted runs after every upgrade. A run that the new code cannot replay fails at the step that differs, with an error that names it. - **The 0.6.0 upgrade completes on an installation with older agent calls.** Its billing-source check failed on calls from before provider-namespaced models. The check now applies to new calls, and older calls stay unbilled. - **A saved browser profile keeps its login cookies.** The launcher writes the session-only cookies at shutdown and adds them back at launch. - **A templated sandbox boots its template image.** Druks sent the installation image with the template, so Drukbox booted the plain image and lost the setup. - **Six open dashboard tabs no longer block a seventh.** A hidden tab closes its live streams and opens them again when shown. - **Hosted deployments run the current Drukbox janitor**, so expired sandboxes are removed. - **A page `Link` to an `/api/` path loads from the server.** Before, the shell handled the click and nothing happened. - **A bare repository name in a ticket matches only that name.** `%` and `_` no longer act as wildcards. - **Druks installs `dbos` below 3.** DBOS 3 refuses the queues at import. - **The Software Factory timeline shows one running row** for an active agent call, and a work-item row omits the PR cell until a PR exists. ## [0.6.0] β€” 2026-09-15 ### Added - **Software Factory has its own ticket board.** A project holds tickets, and a ticket holds markdown comments. Drag a card to change its status. The ticket page saves as you type and selects the repository for the pull request. Select `druks` as the tracker, and Ready for Agent starts a build. The build reads and comments on its ticket through the installation's own MCP server. Its token belongs to the run's account, so an agent comment shows the name of that person. - **The Activity feed replaces the Events page.** Activity records queued runs, requests and their answers, and operator cancels. It also records saved results and the facts that apps announce. Search by work label, filter by app, type, and date, and load older rows. A row opens the saved result and links to the work. New rows wait while you read. An app adds its own rows: `AgentOutput.to_event()` names the topic of a saved result, and `Subject.announce()` records a domain fact. - **A live transcript shows when the agent is thinking.** One "Thinking…" line shows the estimated token count until output arrives. - **An agent call can take its own output contract.** Pass `contract=OutputType` when the output fields depend on the input. The agent declaration stays the same. - **An app can run its own CLI with the settings of an agent.** `Agent.get_config()` returns the harness, model, effort, billing, and secret entries of that agent. - **An app setting can offer live choices.** Declare `Annotated[str, Choices(source)]`, and the settings page shows the values of the source as a select. `choice_details` labels each value of a `Literal` setting. `visible_when` takes one shape, `{field: [values]}`. - **Druks runs on Python 3.14.** CI tests Python 3.11 and 3.14. ### Changed - **The dashboard is a command center.** A sidebar lists the work of every app, with search and phone navigation. The Dashboard shows the requests for you first, then failures, then running work, with exact totals. A request opens the app page that `ui.page(..., subject=...)` declares for that subject type. Schedules have a page below Usage, where you change a cadence or pause a schedule. - **Settings are full pages.** Each page has a stable link, and search finds every field. Unsaved edits ask you to save, discard, or stay. App settings open from the gear next to the app name. Connections has tabs for services, accounts, revoked grants, and browser profiles. An account row shows the provider identity and the granted scopes of its MCP grant. - **Execution defaults are shared, and preferences are personal.** Settings β†’ Agents holds the execution defaults for every account. Preferences holds your timezone. One default account runs unattended work, and every run has an account. The upgrade keeps the account that 0.5.0 used for unattended runs. The top-level `timezone` in `druks.toml` sets the schedule timezone, `UTC` by default. These migrations have no downgrade, so back up the database before you upgrade. - **Credentials stay out of the sandbox.** A sandbox holds a placeholder for each credential, and the Drukbox secrets proxy puts the value into each request. This covers API keys, Claude and Codex subscriptions, GitHub tokens, and MCP credentials. Druks keeps every secret in one encrypted vault and gives a token only to a live sandbox. A Drukbox on another server fetches its secrets from `[sandbox].issuer_url`. To upgrade, follow "The secrets exchange and the secrets proxy" in `docs/deployment.md`. - **One directory holds the harness configuration.** `DRUKS_HARNESS_CONFIG_ROOT` replaces the separate Claude and Codex configuration directories. The default is `~/.config/druks/harnesses`, with `claude/` and `codex/` in it. Copy your files there, then run the installer again. - **An agent id includes the app name.** The id of an app's agent is `.`, so two apps can use the same attribute name. A migration moves the overrides of the bundled apps. For any other app, set the agent overrides again in the settings of that app. - **Reviews are part of Software Factory.** The review app is gone, and a migration moves its runs, events, and settings. `POST /api/software_factory/reviews` starts a review, and its MCP tool is `software_factory_review`. The Dashboard shows running and failed reviews in place of the review page. - **Software Factory works more closely with pull requests.** It fills the pull request template of the repository and replaces only its plan block on each revision. A mention of the review handle in an inline review comment or a thread reply requests a review. An existing GitHub App must turn on the Pull request review comments event. A review gets the GitHub MCP server under the review identity. The reviewer can post its verdict and inline comments with it. - **Software Factory reads statuses and assignees from the tracker.** Linear and Jira share five status settings: trigger, in progress, in review, done, and resting. Each setting lists the statuses of the connected tracker. A status that you set in 0.5.0 does not carry over, so select it again. A build runs as the Druks account whose tracker connection is the assignee. This works when the tracker hides the assignee email. - **`RepoWorkspace` clones the repository.** A workflow on a repository declares `workspace_class = RepoWorkspace`, and Druks clones the subject's repository before every agent call. Override `get_repo()` when the subject keeps the repository in another field. - **A database read needs a bound session.** `db_session()` raises outside a request, a step, or another platform-bound session. It no longer opens a session that nothing closes. In a workflow body, read inside a `@step`. `dispatch()` binds its own session. A test request starts with no session, as in production. - **`AgentOutput.get_artifact()` is now `to_artifact()`.** Rename your override. Druks does not call a method with the old name. - **Usage and subscriptions stay current.** Druks polls each subscription every five minutes, also when no Usage page is open. The interval grows to one hour while the quota does not change. Manage shows when a subscription last refreshed its token. A subscription that the provider revokes stays on the Providers page with the reason, the date, and Reconnect. - **JWT identity uses a JSON Pointer.** `identity.jwt_identity_claim` selects a value such as `/traits/email`. The default is `/email`. Before you upgrade, change a bare claim name such as `email` to `/email`. A bare name fails validation. ### Removed - **`App.record_event()`.** Record Activity with `to_event()` on a saved result or with `announce()`. - **`SubjectActivity`, `App.get_subject_activity()`, and `set_run_phase`.** A subject page shows the sandbox phase of its run while the sandbox starts. - **API-key agents on Models.dev providers.** A key for a provider from the Models.dev directory still saves, but an agent on that provider refuses to run. No proven transport carries its placeholder through the secrets proxy. OpenCode and Pi run on Anthropic and OpenAI keys. ### Fixed - **A service connection that the provider revokes offers Reconnect.** Druks disconnects it when a token refresh gets `invalid_grant`. Before, the connection stayed live, and each scheduled run failed on it. - **A step error that DBOS replays keeps its type and message.** Before, some Druks errors came back as a `TypeError` or with a doubled message. - **A run that dies before its cleanup frees its sandbox within one hour**, not at the end of the lease. ## [0.5.0] β€” 2026-09-05 ### Added - **An app page can be pure Python.** `druks.ui` adds pages, blocks, values, fields, forms, and operation-backed actions: an app writes `@page` functions and the shell renders them with no frontend build. Blocks cover text, markdown, cards, tables, lists, facts, metrics, charts, image galleries, timelines, progress, transcripts, files, and run controls; fields cover text, number, select, multi-select, radio, checkbox, file, multi-file upload, and secret. A page or section can follow a subject, or a whole subject type, and the shell replaces just that region when a snapshot arrives, so scroll, focus, and half-filled inputs survive. `GateControls` reads and answers a parked run's gate from inside the page. A scaffolded app now gets a Python landing page and navigation entry by default; a JavaScript frontend stays available as the escape hatch for an app that needs full control of its screen. The full contract is documented at `docs/druks-ui.md`. - **A provider can be connected with an API key, not only a subscription.** Pasting a key creates the provider and its model catalog on the spot; the Models.dev directory is read on demand and cached for a day, only to populate the "add provider" search box, not stored and refreshed on a cron for every listed vendor. A key is the installation's credential, one per provider, held separately from a person's OAuth subscription β€” an installation can hold both, and each shows its own quota, spend, and Replace/Remove or Reconnect controls. - **Two more coding agents: OpenCode and Pi.** Both ship in the sandbox image alongside Claude and Codex, connect through the same API-key or subscription flow, and get their own per-harness command, first-byte timing window, and doctor probe. - **An exe deployment can pull its own sandbox image.** The exe image registry, username, and password are configurable settings, and a sandbox template's label now reads as `-