CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# 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 `<issuer>/services/<slug>/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
`<endpoint>/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
`<DRUKS_DATA_DIR>/apps/<name>` 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.<name>]` or `[services.<slug>]`. `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.<slug>]` 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.<slug>]` 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 `<endpoint>/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 `<app>_<class>`, 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 `<name>_0001`, `<name>_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 <name>`** 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 `<app>.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
`<app>.<attribute>`, 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 `<app>-<script>` instead of a raw file path.
### Changed
- **A provider owns its login, quota, and catalog; a harness only runs on top of
one.** Settings gains a Providers pane: connect Anthropic or OpenAI with a
subscription or an API key, and every model id is namespaced as `provider/model`.
An agent now resolves to a harness, a model, and a billing choice in one place: the
Agents page lists every agent as it resolves, with an override mark and a lock on a
billing cell a key-only harness fixes. The separate Harnesses pane is gone; its
settings moved to the Agents page's defaults and the per-app override cells.
### Fixed
- **An expired login keeps its account email and past expiry on screen and offers
Reconnect**, instead of looking exactly like one that was never connected.
- **An agent call retries once on an invalid response from the harness**, instead of
failing the run outright.
- **software_factory's triage step reads the reviewer's own words instead of a stale
summary**, and each build step gets only the input it needs instead of the whole
conversation.
## [0.4.0] — 2026-08-29
### Added
- **Files are a platform primitive.** An agent contract declares a `File` field.
Druks pulls the file out of the sandbox, stores immutable bytes under a stable
id, and serves it at `/api/files/{id}` behind the identity gate — images, PDF,
and plain text inline, everything else as a download, validated with a SHA-256
ETag. An app keeps the reference on its own row with `FileField` and decides
when to delete it. A file can also arrive as a user upload, not only as an
agent output. Apps write no transport and no file routes.
- **A workflow declares the sandbox it needs.** `sandbox = Sandbox(setup="sandboxes/build.sh")`
ships the environment as a plain shell file next to the app. Drukbox builds a
reusable template from the platform base and that script, identified by content
hash, and a run that waits for the build shows a sandbox-building phase. App
authors do not name provider images.
- **Background work runs as a task.** `@task` is the lighter door next to a
workflow: a durable, replay-recoverable function with no run row, no timeline,
no subject, and no gates. `every="*/15 * * * *"` runs it on a cadence the code
owns, `.enqueue(**kwargs)` runs one from a route, a subscriber, or a workflow
body, and `retries=` sets the retry count for a task or a step. The token and
model refresh chores are tasks now, so they no longer fill the run timeline or
ask the operator for a cadence. Apps declare tasks in `tasks.py`.
- **A browser session with no login.** An app can declare an anonymous session, so
a workflow borrows a real browser for a site that needs no sign-in.
- **Every run shows its transcript.** Each run on the subject timeline has a
transcript toggle that loads on demand — the newest stays open — so a failure
further back is readable. A failed run always states a real reason: when the
crash left no failure record, the run reads the error from its last agent call,
and a crash before any agent call says exactly that instead of a bare "failed".
- **A repo from a template.** `GitHubClient.create_repo_from_template()` generates
a private repo under an owner and returns its full name. A name already taken
returns that repo, so a retry after a partial failure is safe. The GitHub App
needs `Administration: write` on the target owner.
- **The documentation site.** The guides are published at
[docs.druks.ai](https://docs.druks.ai), and `docs/` holds the same pages the
site builds from.
### Changed
- **The pluggable unit is an app, not an extension.** One word runs through the
whole surface: `from druks.apps import App, AppSettings, Secret`, packages
register under the `druks.apps` entry-point group and keep the class in
`<package>/app.py`, the roster and settings routes become `/api/apps` and
`/api/settings/apps` with `app`, `apps`, and `appSettings` wire fields, and
`druks create app` scaffolds the package. The ASGI modules move to
`druks.api.server` and `druks.mcp.server`. One migration renames the events
column and the settings-override key prefix in place. App slugs, the
`/api/<name>` and `/app/<name>` mounts, and GitHub App settings are unchanged.
- **The ship app is `software_factory`.** The name now says what the app does:
a ticket becomes a pull request, planned, built, and gated on the operator
before it ships. The class, app name, durable kinds, MCP tool, routes, prompt
paths, and the per-repo config path follow. A data migration rewrites stored
kinds, event and file app tags, and settings keys, so history and saved
settings survive the rename.
- **The ORM is async end to end.** Every session on the serving loop is a
SQLAlchemy `AsyncSession`: an async engine, a request-owned session committed
at the request boundary, a session per durable step, and async model and read
surfaces throughout. Sync engines remain only for one-shot processes —
migrate, the CLI, and the plain doctor checks.
- **DBOS 2.30.** Queue dispatch and partitioned-queue reads are faster, recovery
is idempotent, and a duplicate `start()` now takes the holder's handle from
DBOS itself instead of looking the holder up through private API.
- **Workspaces are their own module.** `druks.workspaces` holds the `Workspace`
base and `RepoWorkspace`. Both sat in the wrong layer: the base resolves OAuth,
folds the MCP registry, and commits the database — execution policy, not VM
plumbing — while `RepoWorkspace` was buried inside one app, out of reach of the
others. `druks.sandbox.datastructures` keeps only its data shapes.
- **Images live under the repo namespace.** Every image this repo publishes is
`ghcr.io/czpython/druks/<name>` — `sandbox`, `sandbox-sbx`, and `browser`. The
old owner-level packages stay on GHCR, so an existing deployment keeps pulling
until it moves.
- **The shared review gate reads app-neutral.** The in-app review component no
longer speaks in one app's words or claims that a note starts another plan
pass. It states only what holds for every gate: the note goes to the agent as
feedback.
- **The webhook host names the addresses it binds.** `DRUKS_WEBHOOK_BIND` lists
them and defaults to every interface. A host that also terminates TLS with
`tailscale serve` no longer makes the edge crash-loop on a port 443 bind error.
### Removed
- **The packaged Linear MCP entry.** The catalog ships empty. An operator adds
the servers they want through the API, and MCP delivery no longer fails on a
packaged entry that holds no token.
### Fixed
- **The sbx sandbox provider works on a fresh deployment.** The sbx settings and
auth stores are mounted writable and created with the right owner before the
first compose command, so the CLI no longer panics on start and a credential
read no longer fails on a read-only store. The agent toolchain is published as
an sbx template that boots with no environment, so the first clone in a microVM
finds `git` instead of dying on `git: not found`.
## [0.3.0] — 2026-08-22
### Added
- **Logged-in browser sessions.** An extension declares the browser session a
site needs, and the operator signs in once through a Druks-hosted login window
— real Chrome over noVNC, opened on the site itself, with an optional timezone
and an egress proxy (authenticated if needed) for sign-ins that reject
datacenter IPs. A workflow then borrows that authenticated browser; the login
is encrypted, stored the first time a run reaches for it, and reused across
runs, and a session set to persist saves its refreshed state after each borrow.
- **OAuth connections.** You can sign in to an OAuth service more than once, and
Druks keeps each account as its own connection — one per mailbox, handle, or
workspace, owned by the account that consented and labeled with the provider's
own facts. A Connections page in Settings lists them; disconnecting keeps the
row as a revoked, auditable record rather than deleting it, and a repeat
sign-in reuses the matching connection instead of duplicating it. Extensions
build on one engine, `OauthClient` in `druks.services`, which runs the
authorization-code-with-PKCE flow with token caching and refresh; MCP sign-ins
ride the same engine.
- **Installed apps in the shell.** Every installed app appears in the dashboard
navigation and renders inside the shell instead of a separate page. An app with
a frontend ships an ESM `entry.js` and declared tabs and borrows the shell's
React, components, markdown renderer, and gate controls; an app with no
frontend still gets pages for free — a home page that stacks its subject
boards, and a subject page with summary facts, the run timeline, the latest
transcript, and gate controls.
- **Scheduled workflows can dispatch.** A workflow on a cron cadence can declare
a `dispatch()`; the schedule fires it instead of `run()`, so a subject-backed
workflow resolves its subject and starts the real run without a shim.
### Changed
- **Deployment is one profiled compose file.** Every service lives in a single
`compose.yaml`, with a `hosted` profile for the edge and janitor and a
`gateway` profile for the SSH gateway, so provider choice is a matter of
profiles and `.env`. `SERVICE_TOKENS` renders on every shape with no compose
default, so a missing token stops the sandbox plane instead of it starting on a
known one, and `install.sh` seeds an empty `compose.override.yaml` it never
overwrites, so host-local services survive installs and upgrades.
- **A service keys on a derived slug.** A service no longer declares a name or
title; the slug comes from the class name — `GoogleCalendar` becomes
`google_calendar` — and the card heading derives from it. The wire field and
connect route (`/api/services/{slug}`) follow, and an extension that still
declares `name` or `title` is rejected at load.
- **Board reads know the calling account.** A board scopes its rows to the
operator reading it, a subject carries a readable label, and a subject page and
the free board lead with the subject's type and name — `pull request:
owner/repo#7` — instead of a bare id.
- **One control frame across the shell.** The accent color reaches every surface,
and the status glyph pulses whenever any run is still going.
- **MCP tool names derive from `operation_id`.** An agent-tagged route gives an
unprefixed `operation_id`, and Druks prefixes it with the extension name, so an
author no longer writes the prefix by hand.
- **An app's subjects are checked at load.** A declared subject must supply a real
read-side; the loader rejects the platform stub and fails the boot instead of
the first request.
- **An approved, clean PR merges directly** instead of looping back through the
work gate; only a PR with unmet requirements falls back to auto-merge.
### Removed
- **The per-shape compose files.** `compose.local.yaml` and `compose.remote.yaml`
fold into the one profiled `compose.yaml`; `install.sh` removes the retired
overlays on upgrade.
### Fixed
- **Connect forms draw every declared field.** A bool, int, `Literal`, or
multiline field renders through the shared field component wherever it appears,
instead of falling back to a free-text box, and the replace placeholder
reflects the stored secret.
- **A spend-controlled Codex plan reports its quota as a weekly window** instead
of reading as a parse failure.
- **Sandbox provisioning failures retry.** A transient provisioning error is now
classified as transient, so the in-run retry covers it.
- **The app engine pool is sized for the run queue**, not a single request.
## [0.2.0] — 2026-08-10
### Added
- **One-command install.** `curl -fsSL https://druks.ai/install.sh | bash` boots
the local shape: Docker sandboxes by default, no prompts, no TTY. The local
drukbox runs in compose on the same Postgres, so there is no second datastore.
- **Agent-runbook install.** `INSTALL.md` walks the install as a deterministic
sequence a coding agent can follow; the README carries the prompt to paste in.
- **GitHub App creation from the dashboard.** The connect card creates the App
through GitHub's manifest flow and stores its credentials. Installing the App
is the only step left on GitHub.
- **Service identities.** Connections to outside services live in encrypted
rows an extension can declare. GitHub and the trackers ride them, and a
Services settings pane shows each connection as a card.
- **Agents find and start work.** `list_open_subjects` shows an agent what is
in flight, `ship_start` triggers a ticket, and the surface teaches its own
contract.
- **Adaptive plan gate.** The planner reports its confidence. A confident plan
the critic approved implements without parking; anything less waits for the
operator.
- **Reviews without a review identity.** When no review account is connected,
reviews post as operator comments instead of failing the run.
- **Sandbox commits carry identity.** Commits are authored by the connected
App's bot, with the dispatching account as co-author.
### Changed
- **Plans are briefings.** One critic pass, one redraft, outcome-level
acceptance criteria, one screenful. A parked critique survives an operator
redraft.
- **One review per revision.** The code-review pass folds into the
implementation review as a second lens, and one GitHub review carries the
verdict and the notes.
- **The PR body is written once** and republished on every revision, and it
states the acceptance criteria.
- **`ship_start` stamps the tracker.** It moves the ticket to the trigger
status and lets webhook intake open the build. A ticket Druks has never seen
is a typed 404.
- **Settings panes share one design.** Services, Harnesses, and Skills use one
type scale, one card surface, one status indicator, one chevron.
- **The agent call page shows the call's artifact.** A plan renders on the call
page, not only while its run sits parked.
- **Project delete confirms, then cascades.** Deleting a project removes its
work items after a confirmation that names the scope, instead of a silent 409.
- **`druks doctor` exits non-zero only on a genuine fault.** Pending operator
setup — connect GitHub, connect a harness — reports as pending and exits 0.
### Fixed
- **Live transcripts parse incrementally** instead of re-parsing the whole
buffer twice a second.
- **Ticket intake is idempotent.** A re-delivered "Ready for Agent" webhook no
longer re-builds a merged item.
- **Cancelled builds settle their work items.**
- **Pinned verification commands keep the CI check that proves them.**
## [0.1.0] — 2026-08-02
### Added
- **Multi-tenancy.** Each person signs in through the edge, connects their own
Claude and Codex subscriptions, and sees their own quota and spend.
- **MCP endpoint.** Druks serves `/mcp`. Point an agent at it with a personal
access token to answer gates, retry runs, and read usage.
- **MCP server installs.** Search the official registry from Settings and install
a server by name, connected once for everyone or separately per account.
- **`druks.toml`.** One authored config file; the installer renders `.env` from it.
- **Review extension.** A second bundled app. It reviews a pull request against a
checkout of the repo and posts one GitHub review.
- **Plan gate.** Choose who approves a plan: a human, the machine, or the machine
and then a human.
- **Failure recovery.** Agent calls retry transient harness failures and wait out
quota windows. A run that died can be retried from the step that killed it.
- **Encryption at rest** for harness credentials, MCP headers, and secret settings.
- **Extension testing.** Installing Druks registers pytest fixtures, so an
extension tests itself without a Druks checkout.
### Changed
- **`build` is now `ship`**, packaged under `druks.contrib.ship` and served at
`/api/ship`.
- **Trackers belong to ship.** An installation names one tracker and holds its
credentials in the dashboard.
- **Subjects.** A workflow declares what its runs are about, and the subject
answers for its own status, timeline, and board.
- **The journal replaces workflow state.** A run records typed facts and
announces them.
- **Models come from the provider**, not a list shipped in the code.
- **The events feed is one global timeline.**
### Removed
- **The scoper** — no brief agent, no refinement statuses.
- **Environment-variable configuration.** Secrets, endpoints, GitHub App ids, and
tracker credentials moved to `druks.toml` and dashboard settings.
- **`druks.ticketing`** from the platform. A tracker now only pushes status.
- **`Workflow.set_state()`, `Extension.format_event()`, and
`Extension.subject_type`**, replaced by the journal, feed facts, and the
subject class.
- **Jira remote links and sub-task creation.**
## [0.0.1] — 2026-07-12
Initial public release.