Skip to content

Releases

v0.11.4

Choose a tag to compare

@github-actions github-actions released this 18 Oct 12:04
Immutable release. Only release title and notes can be modified.
2d510df

Patch Changes

  • hosted — #190 36bbe5d Thanks @mattallty! - The hosted Worker's own /healthz now also answers with versionId, the id of the Cloudflare Worker version serving the request, through a new version_metadata binding (CF_VERSION_METADATA) in its wrangler.json. A deploy that tests a version at 0% through Cloudflare's version override header compares it with the version it uploaded, which proves the request reached that version even when the release is the same as the one running. A deployment of your own needs nothing new: Cloudflare provides the binding.
  • hosted — #188 828f086 Thanks @mattallty! - The hosted Worker's own /healthz, at the root of its host, now answers with the release it runs ({ "ok": true, "version": "<release>" }), as each Workspace's /healthz already does. A deploy that tests a new version through Cloudflare's version override header reads it to prove the request reached that version.

v0.11.3

Choose a tag to compare

@github-actions github-actions released this 17 Oct 10:01
Immutable release. Only release title and notes can be modified.
026c67f

Patch Changes

  • core, web — #173 2f3bc0d Thanks @mattallty! - An Agent's own webhook is now sent only the Events of the Projects the Agent was granted. Until now it was sent every Event in the Workspace, including those of Projects the Agent cannot see and Workspace-level ones such as Members, invitations and Sockets; those no longer reach it, and a Project's Events stop the moment its grant is withdrawn. A runtime that only wakes on its own work, as the reference runtime does, needs no change: a Run handed to it, its Gates and their rulings all arrive as before, as long as the Agent is granted the Project. Generic subscriptions under Settings › Webhooks are unchanged. The Agent's page has a new Webhook section, for its Sponsor and admins, that sets the URL and the secret deevy signs deliveries with, generates a secret and shows it once, moves the deliveries without a new secret, and stops them; deevy agents update --webhook-url --webhook-secret still works.
  • core — #177 1472945 Thanks @mattallty! - Saving a Project's Checkpoints no longer cuts its open Gates loose from them. Every save used to replace every Checkpoint, so afterwards a Gate's Slack message and its email said it wanted one approval, whatever its Checkpoint asked for, while deevy itself still counted the real number. A Checkpoint that stays on the list is now changed in place and keeps its id, so a Gate's message and email name the approvals its Checkpoint wants, lowered or not. A Checkpoint you remove still lets go of its Gates, which then want the default's one approval everywhere. A Checkpoint you add at a name an open Gate already asked at now counts for that Gate in Slack and email too. For API readers, checkpoints.list and checkpoints.set keep each Checkpoint's id across saves.
  • core, web — #175 f50792e Thanks @mattallty! - At a Checkpoint with Not the Human the work is for on, that Human can now reject the Gate, in deevy, with /reject on the record or with Slack's button; only their approval is refused, and the refusal says they may still reject. Before this, they could do neither. They are still not asked in their Inbox, and the Gate stays off their Needs me. A Gate left short of Humans who could approve it, usually because somebody was suspended after its Checkpoint was set, now says so above Approve with both numbers, for example "This Gate still wants 2 approvals and only 1 Human can still give one", and says that an admin can lower Approvals on the Checkpoint or reinstate a suspended Member. Lowering a Checkpoint's Approvals, or removing the Checkpoint so it falls back to one, now passes every open Gate there that already has that many approvals: the Run goes on, the Agent is told, and the record and Slack are updated, all as the admin who saved, saying the threshold was lowered. Turning the exclusion off counts no approval that was refused. For API readers, a Gate (gates.get, gates.list, gates.request, gates.approve, gates.reject) gains you.mayApprove, you.whyNotApprove and shortfall ({ wanted, couldApprove, why }, or null). you.mayRule and you.why still mean whether you may rule at all and why not, so for the excluded Human mayRule is now true and mayApprove false. gates.list({ mine: true }) returns the Gates you could approve, as before. The message refusing the excluded Human's approval now ends "to approve it; you may still reject it". A Gate passed by lowering Approvals has loweredFrom, the old threshold, in the payload of its gate.approved and run.answered Events, which webhooks deliver.
  • core — #172 709241d Thanks @mattallty! - An Agent is given work only in the Projects it was granted. A comment mentioning it, or a record routed to it, in a Project it was not granted used to start a Run it could never see (it waited, pending, for good); now nothing starts. issues.create, and with it deevy issues create --assign-agent, refuses to assign such an Agent, saying to grant it the Project first. And taking a Project back from the Agent that is its Default Agent now leaves the Project with no default, with a project.updated Event saying so, instead of routing records to an Agent that cannot work them.
  • core, web — #174 5324623 Thanks @mattallty! - Settings › General offers a GitHub organization rule only when your team can sign in here with GitHub, and a GitLab group rule only when it can sign in with GitLab: each is checked against the account somebody signed in with on that forge, so without that sign-in it let nobody in. A rule you saved before its provider was taken away stays listed, marked as letting nobody in until people can sign in with that provider again. allowlist.add, and with it deevy allowlist add, now refuses such a rule with PRECONDITION_FAILED and a message saying why, where it used to save it.
  • core, sockets, web — #178 0b43b06 Thanks @mattallty! - A Socket's answers no longer carry its previous webhook secret, which every sockets.* answer and the API reference included, sealed but still a stored secret, once the secret had been replaced; nor when it was replaced, which nothing read. Mint a webhook secret (sockets.rotate) now keeps the old secret working for a day, as pasting a new one already did, so deliveries signed with it are not refused while you paste the new one into the tool. And sockets.rotate now refuses a Slack, Linear or Notion Socket, from the API and the CLI too, saying what to do instead: Slack and Linear make their own signing secret and take none typed in, so you paste the tool's new one on the Socket's page; Notion signs with its verification token, which a minted secret would have replaced. sockets.providers says which way each tool works, in a new webhookSecretFrom field (either, tool or handshake), and the Socket's page offers what that says. A Socket module of your own must now declare webhookSecretFrom.
  • design, web — #185 f55482c Thanks @mattallty! - Text in deevy's colours now meets WCAG AA contrast (4.5:1) in both themes, on every ground the app puts it on, its tinted badges included: an Agent's name and the Working badge, the Finished and Failed badges, a destructive button, a Human's initials, a pressed toggle, hints in the sidebar and white on an indigo button were each under it, mostly in the light theme. The colours keep their hue and lose a little lightness, and the logo's two halves are a shade deeper in the light theme to match.
  • design, web — #182 88aa8bc Thanks @mattallty! - deevy's logo is indigo and rose: the Human half takes the app's indigo, and Humans are indigo throughout the app (their avatars, their names in a feed), where they were sky blue. Indigo now means a Human and what a Human does, rose an Agent, amber a Gate. In the dark theme, buttons are the same indigo as in light with white text on them, and links and a Human's name use a lighter indigo so they read on the dark page. The favicon and the home-screen icon change with the logo.
  • web — #176 54e7e04 Thanks @mattallty! - Connect GitHub can make the App on an organization. Give it the organization's login in the new Organization field and the manifest goes to that organization's settings on GitHub, so the App belongs to the organization and its repositories can install it; until now the App was always made on your own account, and as a private App it could never be installed on an organization's repositories. Only an owner of the organization or one of its GitHub App managers can make it there. A Socket's page also changes its signing secret the way each tool allows, under What it signs with: where the tool makes the secret (Slack, Linear, and GitHub or GitLab when you change it there) you paste the new one into New secret from the tool, and deevy keeps accepting the old one for a day, so the two can change in either order. Mint a webhook secret is offered only for GitHub and GitLab, whose settings can take a secret deevy made, and a Notion Socket no longer offers it at all, since a minted secret would replace Notion's verification token and break its webhook.
  • server — #170 5afa3c4 Thanks @mattallty! - The repository's docker-compose.yml now asks for DEEVY_SECRET, as it already as...
Read more

v0.11.2

Choose a tag to compare

@github-actions github-actions released this 14 Oct 15:49
Immutable release. Only release title and notes can be modified.
4123f88

Patch Changes

  • core — #164 4796865 Thanks @mattallty! - The API reference at /api/docs works again for a deevy served under a path and for every hosted Workspace (/acme/api/docs). Since 0.11.0 it was sent the policy every other page carries, which refuses the reference viewer's script, so the page stayed blank; at the root of a host it was unaffected.
  • core, web — #163 288dbe6 Thanks @mattallty! - Opening or reloading a Gate, a Run, a record, a Project, a Settings page or an invitation link no longer shows a blank page, and neither does a deployment's own address under a path when it has no trailing slash (/acme). Since 0.11.0 the security policy every page carries refused the page's own <base href>, so the browser looked for deevy's scripts under that page's path. It now allows a base on deevy's own origin. Every deployment was affected: the image, the Worker, and hosted Workspaces.
  • web — #162 7526cb2 Thanks @mattallty! - deevy has a logo: a "d" made of a Human's half-disc and an Agent's bar, beside the word in Outfit. It is the favicon and stands on the sign-in page, and page and dialog titles are set in Outfit to match. Two new files are served beside the app, at the root of its address or under its path: favicon.ico and apple-touch-icon.png. A proxy that lets only known paths through needs to allow them, the way it allows favicon.svg.

v0.11.1

Choose a tag to compare

@github-actions github-actions released this 11 Oct 20:50
Immutable release. Only release title and notes can be modified.
7646ff7

Patch Changes

  • hosted — #158 8c8a7a5 Thanks @mattallty! - The hosted Worker's archive, deevy-hosted-<version>.tar.gz with its checksum and attestation, is on the GitHub Release again. 0.11.0's Release went out without it, so 0.11.1 is the first release a hosted deployment can deploy.

v0.11.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 20:21
Immutable release. Only release title and notes can be modified.
40d1a22

Minor Changes

  • core, adapters, web — #142 9e267b9 Thanks @mattallty! - A page now runs only the scripts deevy shipped, and a change made with your session must come from deevy's own page. Every page carries a Content-Security-Policy that allows scripts from deevy's origin only (no inline script, no eval, nothing from a CDN) and lets nothing frame it, so a Gate's Approve cannot be clicked through someone else's site; beside it come X-Frame-Options: DENY, X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin, and every API answer is sent as something never to render. The Docker image sets them itself; on Cloudflare they ship in the build's _headers file, which the asset handler applies. /api/docs loads Scalar from jsDelivr under a policy of its own. If a proxy sits in front of deevy, let these headers through unchanged and do not inject scripts into its pages: an analytics snippet, Cloudflare's Rocket Loader, Email Address Obfuscation or Web Analytics injection will be refused by the browser.
    A POST, PUT, PATCH or DELETE to /rpc or /api that your session cookie signed in is refused with 403 unless the browser says it came from the same origin (Sec-Fetch-Site: same-origin), or sends an Origin of BETTER_AUTH_URL or DEEVY_WEB_ORIGIN (with no BETTER_AUTH_URL, the address the request reached). A proxy that strips both headers or rewrites Origin will make every change in the app fail, and an SPA served from an origin of its own needs that origin in DEEVY_WEB_ORIGIN, as it already did. Agents' API keys and CLI or MCP access tokens are unaffected, and /mcp no longer accepts a browser session at all, only a bearer.
    A sign-in provider's access and refresh tokens are now encrypted at rest under BETTER_AUTH_SECRET. Tokens stored before the upgrade stay readable and are encrypted the next time that person signs in with the provider; nothing needs migrating. A Link must now be an http or https address, and a link to anything else that a tracker or an Agent hands deevy is shown as text rather than as a link.
  • core, web — #136 4544e95 Thanks @mattallty! - Settings shows the version deevy runs and links to its source; /healthz reports the version. The foot of every Settings page reads deevy <version> · Source code, and the link goes to that release's tag on GitHub (the repository itself on a build that does not say its version), so whoever uses an instance can read the source of what they are using, as the AGPL asks. /healthz now answers {"ok":true,"version":"<version>"}, and health.ping has a version field; both say null when the build did not name one. A probe that only checks ok or the status code needs no change.
  • web — #150 c2ad0ec Thanks @mattallty! - A browser tab that stays hidden for 30 seconds stops listening for live updates, and picks up where it left off when you come back to it: what changed while it was hidden appears at once, and a tab left open in the background no longer holds a connection to the server all day. On a hosted Workspace, an open tab now hears about new Events over a WebSocket the Workspace pushes to, rather than a stream it keeps open, so a Workspace sleeps between Events while its tabs stay open. health.ping says which a deployment offers in a new live field: stream for the Docker image and a Worker of your own, which work as before, and websocket for a hosted Workspace. A tab that cannot open the socket, behind a proxy that drops WebSocket upgrades say, uses the stream instead.
  • server — #138 8b8cb4d Thanks @mattallty! - deevy can live under a path of its host, such as https://company.example.com/deevy behind a reverse proxy: set BETTER_AUTH_URL to that URL and every route, the sign-in callbacks, the session cookie, the OAuth issuer and the SPA follow it. Discovery for a Human's MCP client stays at the root of the host (/.well-known/oauth-authorization-server/deevy), so a proxy that forwards only the path must forward those documents too, and a Worker under a path sets assets.run_worker_first to true; see "Under a path" in OPERATIONS.md. A deployment at the root of its host behaves exactly as before. The SPA's built files are now relative to a <base href> the server writes, so a proxy that rewrote asset URLs no longer needs to.
  • server — #147 4f81af8 Thanks @mattallty! - The many-Workspaces Worker limits what each Workspace may do in a day, since they all send through one sender: past DEEVY_HOSTED_INVITATIONS_PER_DAY invitations in the last 24 hours (50 by default) the next is refused with when it can be made, and past DEEVY_HOSTED_EMAILS_PER_DAY emails (500) what is owed waits and goes out as the window opens, which Settings › Email shows. Platform.configure gives one Workspace limits of its own, and Platform.status reports the limits in force with invitations and emails today and Runs this month. The image and the Worker limit nothing, as before. See "The many-Workspaces Worker" in OPERATIONS.md.
  • server — #148 41b97b6 Thanks @mattallty! - Every release now carries the many-Workspaces Worker ready to deploy: deevy-hosted-<version>.tar.gz on the GitHub Release, with its .sha256 and a build provenance attestation (gh attestation verify deevy-hosted-<version>.tar.gz --repo WeAreNendo/deevy --signer-workflow WeAreNendo/deevy/.github/workflows/hosted-release.yml). Inside are the bundle and its source map, the SPA, a wrangler.json with what only the deployer knows left as marked blanks (<<HOST>>, <<DIRECTORY_KV_ID>>, <<CONSOLE_SERVICE>>), and a manifest.json naming the version, the commit, the compatibility date and flags, the Durable Object class and its migrations, every secret and variable the Worker reads, and the SHA-256 of every file. A hosted Workspace's Platform.status reports the version its build was made from. See "The many-Workspaces Worker" in OPERATIONS.md.
  • server — #145 2df3151 Thanks @mattallty! - A third way to run deevy: apps/hosted, one Cloudflare Worker that serves many Workspaces on one host, each under its own path (https://app.example.com/acme) with its database in a Durable Object of its own. Workspaces are provisioned, suspended, dumped and removed through a Platform service binding, sign in through one relay registered with every provider, and run their background work on their own alarms. The image and the Worker are unchanged. See "The many-Workspaces Worker" in OPERATIONS.md.
  • server — #144 e5b97a1 Thanks @mattallty! - Several deevy deployments can share one set of sign-in provider Apps through a relay: set DEEVY_SIGN_IN_RELAY_SECRET on all of them, DEEVY_SIGN_IN_RELAY_URL on each deployment that signs in through the relay, and DEEVY_SIGN_IN_RELAY_ALLOW on the one that is the relay, then register ${DEEVY_SIGN_IN_RELAY_URL}/callback/<provider> as each App's callback. The relay only redirects; each deployment still exchanges its own codes. A deployment's own OpenID Connect provider and a self-managed GitLab are never relayed. Nothing changes for a deployment that sets none of these. See "Signing in through a relay" in OPERATIONS.md.
  • server — #149 3b0b7f6 Thanks @mattallty! - A hosted Workspace can be taken home. The image carries dist/import.mjs, which loads the dump a hosted export hands over into a new volume: docker run --rm -i -v deevy-data:/data ghcr.io/wearenendo/deevy dist/import.mjs - < workspace.sql. It refuses a database that already holds anything, brings a dump from an older release forward with the image's own migrator and refuses one from a newer release, removes the OAuth resources and tokens bound to the hosted address, checks DEEVY_SECRET against everything sealed with it when that is set, and prints what it imported and what to point at the new address. Start deevy on the volume with the BETTER_AUTH_SECRET and DEEVY_SECRET the export handed over. dist/import.mjs --for-d1 turns the same dump into SQL for a new D1 database, for a Worker of your own. See "Taking a hosted Workspace home" in OPERATIONS.md. Nothing changes for an instance that does not import.
  • server — #141 6692d80 Thanks @mattallty! - Every release from this one on upgrades in place, and can be rolle...
Read more

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 09:12
Immutable release. Only release title and notes can be modified.
7299437

Minor Changes

  • core, db, web, server — #125 52dd90a Thanks @mattallty! - Choose what deevy emails you, under Settings › Notifications. A new Email column sits beside Inbox, Slack and Direct message, one switch per kind. Until you change them, email is on for a Gate awaiting you and a Run awaiting your answer, and off for the rest. The page says which address email goes to; if your sign-in didn't confirm an address, it says deevy can't email you and the switches are off.
    Every email can be stopped from the email itself. Each carries a one-click unsubscribe (RFC 8058) that turns off that kind of email for you alone, and leaves the inbox and Slack as they were. Opening the link only asks; the change happens when you press the button, so a mail scanner opening links does nothing. The link is signed with BETTER_AUTH_SECRET and works for six months.
    The migration adds a nullable email column to notification_preference, applied at startup on Docker. On Workers, apply the D1 migrations before deploying, as for every release.
  • core, email, web, server — #124 2494adf Thanks @mattallty! - deevy can now email a Human when a Gate or a Run waits on them, through Resend for now, with more senders to come. Set DEEVY_EMAIL_SENDER=resend, DEEVY_EMAIL_FROM (an address on a domain Resend has verified, such as deevy <deevy@yourcompany.com>) and RESEND_API_KEY, on the Docker image or as Worker secrets. A sender set up halfway stops the Docker image at startup, naming what is missing; a Worker stays up and says so under Settings › Email.
    A Human is emailed at the address their sign-in provider verified, never at one it did not: a Linear or Atlassian sign-in gets no email. Until they can choose under Settings › Notifications, they get an email for a Gate awaiting them and for a Run awaiting their answer, and nothing else. A Gate's email names the Agent, the Checkpoint and the record, quotes the start of the Proposal, and links to the Gate in deevy, where they rule as before.
    An email the sender refuses for good (a bad key, an unverified domain) is given up on at once and recorded in the Event log as email.exhausted, in the sender's own words. With no sender configured, nothing is queued up for later.
  • core, db, web — #131 2c29f02 Thanks @mattallty! - Invitations are emailed now, when a sender is configured. Inviting someone emails them the link, saying who invited them, to which Workspace, as what, and until when. The dialog still shows the link once, and says either that it is being emailed or why it wasn't, so you can send it yourself. Each outstanding invitation shows whether its email went: Emailed, Email on its way, or Email failed with the sender's own words. Pass send: false to invitations.create to only get the link. To email it later, the link's token is kept sealed under DEEVY_SECRET, and only until the email lands or the invitation is accepted, revoked or expires; without DEEVY_SECRET the invitation is not emailed, and the dialog says so. A migration adds invitation.sealed_token, which no read ever returns.
  • core, db, web, server — #128 29c750e Thanks @mattallty! - Settings › Email shows which sender this Workspace sends through, as which From address, and whether that was set here or in the environment. Send a test email mails it to you, and says where it went or what the sender refused. An admin can also set the sender here, which overrides the environment's until they press Use the environment's sender. Its key is sealed under DEEVY_SECRET like a connected tool's credentials, and never shown again; leave it empty to keep the saved one when you only change the From. A sender that can't be built, or can't run on this deployment, is refused with the reason before it is saved. The page is admin-only and can only be changed from a signed-in session. The API is email.status, email.configure, email.clear and email.test, and the Event log records email.configured and email.cleared (without the key). A migration adds the email_sender table.
  • core, db, web, server — #127 eba859a Thanks @mattallty! - Route Notifications to a team email address, like a Slack room. Under Settings › Channels, Add an email address takes a shared mailbox, such as your approvals list. deevy mails it a confirmation link straight away and tells you what the sender said, so a domain it can't send from shows up there and then. Nothing is sent to the address until somebody there confirms; opening the link only asks, and the button confirms. Once confirmed, your routing rules reach it like any Channel: one email per Notification, whoever it concerns, ending with who routed it there and where that stops. While it waits, Test sends the confirmation again; once confirmed, it sends a test email. The API is channels.createEmail, and every Channel now reports address and confirmedAt.
  • adapters, email, web, server — #130 066de9d Thanks @mattallty! - Two more ways to send email, each where it can run.
    • SMTP, on the Docker image. Set DEEVY_EMAIL_SENDER=smtp and SMTP_URL, such as smtps://user:password@mail.example.com:465, or smtp://…:587, which upgrades with STARTTLS. You can also choose "SMTP server" under Settings › Email. A server's 5xx refusal is given up on in its own words; a 4xx, or a server that doesn't answer, is tried again.
    • Cloudflare Email Service, on Workers. Uncomment the send_email binding named EMAIL in wrangler.jsonc and set DEEVY_EMAIL_SENDER=cloudflare. No third-party account or key is needed. It is a Cloudflare beta, and reaching an address that isn't verified on your account needs the Workers Paid plan. On an instance served over plain http, emails go without the one-click unsubscribe header, which Cloudflare refuses there; the footer link still works.
      A sender a deployment can't run (SMTP on Workers, Cloudflare on Docker) is refused with the reason, under Settings › Email and at startup.
  • email, web — #129 a782f32 Thanks @mattallty! - deevy can now send email through Postmark, SendGrid, Mailgun or Amazon SES as well as Resend, on either deployment. Set DEEVY_EMAIL_SENDER to postmark, sendgrid, mailgun or ses, or choose one under Settings › Email:
    • Postmark: POSTMARK_SERVER_TOKEN, and optionally POSTMARK_MESSAGE_STREAM (default outbound).
    • SendGrid: SENDGRID_API_KEY.
    • Mailgun: MAILGUN_API_KEY and MAILGUN_DOMAIN, and MAILGUN_REGION=eu for a domain in Mailgun's EU region.
    • Amazon SES: AWS_SES_REGION, AWS_SES_ACCESS_KEY_ID and AWS_SES_SECRET_ACCESS_KEY, ideally for an IAM key that may ses:SendEmail and nothing else. Requests are signed in deevy itself, so it needs no AWS SDK and works on Workers.
      A refusal the sender says is permanent (an unverified domain, a bad key, a suppressed recipient) is given up on at once, in the sender's own words; a throttle or an outage is tried again.

Patch Changes

  • server — #122 7edb623 Thanks @mattallty! - The server image runs on a rebuilt base with OpenSSL's October fixes (libssl3t64 3.5.7-1~deb13u3), closing CVE-2026-75804 (a denial of service through QUIC flow control) and CVE-2026-84782 (an information disclosure through DTLS handshake retransmission). Nothing to change on your side: pull the new image.

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 20:28
Immutable release. Only release title and notes can be modified.
595b5dd

Minor Changes

  • core, web — #112 74f2381 Thanks @mattallty! - An Agent's page now tells its Sponsor what the Agent costs. A new Usage section shows this month and last:
    • how many Runs there were, and how many reported nothing;
    • the cost as the clients' estimate, with the tokens it came to and how many no cost covers;
    • the working time, with the waiting time in the tooltip;
    • per finished Run, the average cost and working time. The average cost is taken over the finished Runs that reported one, so an unpriced Run never quietly lowers it.
      Only the Agent's Sponsor and admins see the section. The same numbers come from the new agents.usage operation (GET /agents/{memberId}/usage, up to twelve calendar months in UTC, counting each Run in the month it was created).
  • core, sockets — #118 2425e82 Thanks @mattallty! - Somebody who signs in with Linear or Slack now rules from there with no linking step, as somebody who signs in with GitHub already does from github.com. A Linear sign-in counts for comments in the workspace it was to. A Slack sign-in counts for button clicks only when Slack's sign-in named the Slack Socket's own team. A guest from another team in a shared channel still needs a link code. Everybody else links as before: Linear on Linear's own page, Slack with the code Slack sends them. See ADR-0027.
  • core, web, server — #116 480d048 Thanks @mattallty! - deevy can now offer Microsoft, Linear, Slack and Atlassian sign-in, beside GitHub, Google, GitLab and your own OpenID Connect IdP. Set MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET (and MICROSOFT_TENANT_ID to narrow the accounts it takes: organizations, or your tenant's id), LINEAR_, SLACK_ or ATLASSIAN_ pairs. Each one's redirect URI is ${BETTER_AUTH_URL}/api/auth/callback/<id>.
    What each provider can let in depends on whether it vouches for the address:
    • Slack does, so an email_domain rule admits a Slack sign-in.
    • Microsoft does only when the tenant sends the optional verified_primary_email and verified_secondary_email claims; docs/OPERATIONS.md says where to add them. Without them, a Microsoft sign-in comes in by invitation.
    • Linear and Atlassian never do, so they come in by invitation only.
      Providers are offered in the order an engineering team reaches for them: GitHub, Google, Microsoft, GitLab, Linear, Slack, Atlassian, and your own IdP last. DEEVY_SIGN_IN_ORDER puts the ids you name first, e.g. oidc,github for a company that signs in through its IdP. deevy also remembers which provider a browser signed in with last, in a cookie holding only the provider's id.
  • core, db, web — #106 8088cf2 Thanks @mattallty! - A Run can now say what it spent. Whatever runs an Agent reports what one session of a Run spent with runs.reportUsage (POST /runs/{runId}/usage, and the MCP tool runs_report_usage): tokens by model, prompt-cache reads and writes kept apart, and a cost where its harness priced them. A session reports under a key of its own, and the same key again replaces what it said, so a retried call counts once. Only the Run's own Agent may report, and a Run takes reports until a day after it finishes.
    runs.get and runs.list carry the Run's totals as usage, and runs.get breaks them down by model. deevy never prices a token itself: a cost is the sum of what was reported, it is null where nothing was, and unpricedTokens says how much of a Run no cost covers. The Event log records each report as run.usage_reported.
    A new table, run_usage, is applied by the migrator at startup; on Workers, apply the D1 migrations before deploying, as for every release.
  • core, db — #107 0e3d48f Thanks @mattallty! - Every Run now says how long it took. Each Run in the API carries timing: queuedMs, from when it was created to when it started; waitingMs, the time it spent waiting on a Human, at a Gate or on a question, added up over every wait; and workingMs, the rest of its life. A Run still going counts until now, and a finished one stops at its finish. A stale Run's silence counts as working time, because nobody was waiting on a Human through it.
    deevy works this out itself, so it is there for every Run, whatever ran the Agent. Two new columns on run are applied by the migrator at startup. A Run that was already waiting when you upgrade counts its wait from its next change.
  • core, db, sockets, web, server — #104 449cd6f Thanks @mattallty! - deevy no longer keeps a tracker of its own. The work stays in GitHub, Linear, GitLab or Notion, and deevy routes it to Agents, records their Runs, and holds the Gates only a Human may rule on.
    Upgrading from 0.8 means starting again. The migration history starts afresh, so 0.9 runs on an empty database — a new volume, or a new D1 database on Workers — and there is no upgrade path from 0.8. Nothing is exported: the old volume keeps what it had. docs/OPERATIONS.md, "Coming from 0.8 or before", says what to do.
    Set DEEVY_SECRET to at least 32 random characters before connecting anything: every tool's credential is sealed under it. Keep it apart from BETTER_AUTH_SECRET, and back it up with the database, because losing it means connecting every tool again.
    A Socket is one tool deevy is connected to, under one identity of its own. An Issue is now what a record in a tracker says — its key (acme/deevy#42, ENG-12), its URL, title, body and state — and deevy authors none of it. A Project is a binding: where its records come from, where its code and its documents are, which Agents may work it, the Agent a record nobody named goes to, and its Checkpoints.
    What is gone: deevy's own Issues and DEV-42 keys, the Workflow of States, Documents and their live editing, Labels, Teams, stored comments, the board and the Issue pages. On Workers the Durable Object went with the Documents, so a deployment needs no paid binding and nodejs_compat is off.
    How a record reaches an Agent. A tool delivers to POST /hooks/<socketId>, signed. A label agent:<handle> gives the record to that Agent; otherwise the Project's default Agent takes it. An instance no tool can reach asks each tool what changed instead: DEEVY_SOCKET_CATCHUP_MINUTES (default 30) is how long a tool may be quiet first, and a Socket can poll on an interval of its own.
    deevy says back, where the team reads. A Gate becomes a comment on the record with the Proposal and how to rule, the record carries deevy:awaiting-approval while it waits, and the ruling is another comment with its arithmetic ("1 of 2"). With the Project's Mirror set to runs, Runs starting and finishing are said too; off says nothing. Every such comment ends — <Agent> · <Run> · via deevy, because the tool shows deevy as its author.
    To try it with no account anywhere, DEEVY_DEV_STUB_SOCKETS=1 registers an in-process tracker and forge (docs/DEVELOPMENT.md). The milestone was built in #81–#96, then walked against github.com and checked against what each other tool publishes in #98–#103; the plan and what each step found are in docs/plans/sockets.md, and the reasoning is ADR-0024 and ADR-0025.
  • core, sockets, web — #104 449cd6f Thanks @mattallty! - GitHub, through an App deevy writes for you. Under Settings › Sockets › Connect GitHub, deevy posts the App's manifest to GitHub; GitHub makes the App and sends you on to install it, and deevy seals its credentials without anybody copying a key out of a browser. An App you already have can be pasted instead, its .pem whole.
    A repository in one of the App's installations is a container. Binding a Project to one binds its issues and — unless you untick Its code is here too — its code, from the repository's own base branch. GitHub cannot assign an App, so a label is how a record is given to an Agent. A Human who signs in to deevy with GitHub rules from github.com with /approve or /reject <why>, with no linking step.
    DEEVY_GITHUB_API points every GitHub Socket at a GitHub Enterprise Server. A Socket's page shows the address the App delivers to, and Point GitHub at this address moves the App there when the instance's address changed, as a laptop's tunnel does.
    "Working in GitHub" in docs/OPERATIONS.md walks through it. It was walked end to end against github.com, with Claude Code as the Agent.
  • core, sockets — #104 449cd6f Thanks...
Read more

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 07:37
Immutable release. Only release title and notes can be modified.
804faa8

Minor Changes

  • core, cli — #71 a6c699c Thanks @mattallty! - The CLI now asks the instance what it can do before it asks it to do anything. A command the instance does not have is refused by name — https://old.example.com has no \projects.create`— rather than failing as a 404 you have to interpret. That is what makes a newer CLI safe against an older deevy, which will happen the first time somebody upgrades one and not the other. It reads the instance's own API document once a day and keeps the answer beside the token. It is not a version check: two instances on the same version have the same operations, and comparing the operations says _which_ command is missing. An instance now puts its version in that document, so the message can name both sides. A deevy that does not say (an older one, or one whose entry never told it) is described rather than given a number it did not claim.--deevy-urlpoints a single command somewhere without settingDEEVY_URL. One upgrade note: the file a token is kept in now carries the scheme — https_deevy.example.com.jsonrather thandeevy.example.com.json— becausehttp://host` and https://host are two instances and were sharing one file. Signing in again is all that is needed.
  • core — #67 111803f Thanks @mattallty! - deevy now issues access tokens for two protected resources instead of one: the MCP server at ${BETTER_AUTH_URL}/mcp, as before, and the operation API at ${BETTER_AUTH_URL}/api, which is new. A token is checked against the resource it was minted for, so one granted to an MCP client reaches the tools and not the ninety-four operations behind them, and one granted to an API client is refused at /mcp.
    Nothing an existing MCP client does changes. Its tokens are still minted for /mcp and still accepted there; the second resource is additive.
    This is what lets a Human sign in from something that is not a browser — the CLI being built on top of it — without that credential inheriting everything an MCP consent grants. Gate rulings are unaffected and still cannot be made by any delegated credential, whatever it is for.
    A client is linked to the API resource only if it asks for one at registration, so an MCP client that registers without naming it cannot obtain an API token at all, and one that asks for it anyway is refused with invalid_target. A client that legitimately wants both names both, and then holds a token whose audience is both — which is RFC 8707 working as specified, and the reason "a token for one surface is refused at the other" is a statement about single-resource tokens.
    RFC 9728 protected-resource metadata is served for the MCP resource only. A client that needs the API resource reads /.well-known/oauth-authorization-server and names the resource in its authorization request (RFC 8707). A client identified by a Client ID Metadata Document has no requested resources, so it is linked to MCP alone.
  • server — #65 c8deadb Thanks @mattallty! - The deevy image now runs as an unprivileged user on Google's distroless Node base. It carries Node, deevy's bundle and nothing else — no shell, no package manager, no apt, and no way to get a prompt inside the container. It is about a third smaller for it.
    Upgrading an existing instance needs one command first. Everything on your volume was written as root, and the new image runs as uid 65532, so it can read the database but not write to it. With the container stopped:
    docker run --rm -v deevy-data:/data alpine chown -R 65532:65532 /data
    If you forget, deevy tells you: it checks that it can write /data before it opens the database and stops with that exact command in the error. That check is new in this release and it matters more than it sounds — the migrations on an upgraded volume are already applied and nothing else writes at startup, so without it a root-owned volume would carry the container all the way to healthy and only fail at the first thing anyone tried to save. The command is idempotent; run it if you are unsure.
    A fresh install needs nothing — a new named volume takes its ownership from the image. A host directory you bind-mount does not, so chown -R 65532:65532 that before the first start.
    The image now carries its own healthcheck, so docker ps reports healthy and Compose's depends_on: condition: service_healthy works without a healthcheck of your own, on whatever DEEVY_PORT you set. The one in the published docker-compose.yml has been removed. If you have a copy of the old file, delete the healthcheck: block under the deevy service — it calls node by name, and the new image has no shell and no PATH entry to find it with, so it would report unhealthy forever and hold back anything waiting on it.
    Backups are unchanged: sqlite3 was never in this image and still is not, so the sidecar recipes in docs/OPERATIONS.md are still how you take one. Restoring now needs one more line — a file copied in by a root sidecar has to be given to uid 65532 before deevy can write it. The recipe in the docs does that.
    If you already ran the image with --user or a user: in Compose, it still works; that is now the default rather than something you have to ask for.
  • cli — #66 5ba116f Thanks @mattallty! - The first piece of a deevy CLI: a new @deevy/cli package that reads deevy's operation registry and works out the command list from it. Nothing is runnable yet — the commands themselves, sign-in and output formatting are the slices after this one — but the shape is now fixed, and it is the shape the API, the MCP tools and the web client already have: one operation defined once, projected to a surface (ADR-0005).
    There is nothing to install and nothing changes for a running instance.
  • cli — #72 5825952 Thanks @mattallty! - Commands now answer with something a person can read. A list is a table with the columns you recognise a row by — key, title, handle, and the State's name rather than its id — plus whatever time the row carries, so deevy inbox list shows you what is unread and deevy members list shows you who is suspended. A single thing is its fields, with nested shapes indented under their names. An operation whose whole answer is that it happened says so in a word, and one that counts says the count.
    Free prose stays out of the grid and long values are clipped, so one Issue with a long description no longer makes every other row that wide. Titles that are not ASCII line up, because a column is not a UTF-16 unit.
    --json is unchanged and exact, and is what a script should use.
    Colour appears only when you are looking at a terminal: a pipe gets plain text, and NO_COLOR turns it off.
    None of this is written per command. The shape of the answer decides how it is shown, so an operation added to deevy is readable without anybody teaching the CLI about it.
  • cli — #70 a142f03 Thanks @mattallty! - Every operation deevy has is now a command. deevy issues create --project-key DEV --title "...", deevy issues move DEV-42 --state-id ..., deevy runs list, and ninety more — none of them written by hand. The command name, its help text, which arguments are positional and which are flags, what an enum accepts and what is required all come from the same definition the REST route and the MCP tool come from, so an operation added to deevy is a command with no CLI change at all.
    Path parameters are positional and everything else is a flag: deevy issues get DEV-42, deevy issues list --project-key DEV --assignee-kind agent. A list is one flag given more than once. --json is on every command.
    Four operations answer with an explanation instead of a request. gates approve and gates reject are a Human's, in a browser, and no CLI, key or Agent can rule a Gate whatever it is signed in as. oauth-clients list and oauth-clients revoke want a Human signed in to deevy itself, because a delegated credential should not be able to list or revoke the consents that delegated it. The CLI says which of the two rules it hit rather than passing back a bare FORBIDDEN.
    It also refuses before asking when it already knows the answer: an operation an Agent may not call says that DEEVY_API_KEY is why, and one only an Agent may call says to set it.
    When deevy refuses input, you get the reason the schema wrote — "Two to six uppercase letters, as in DEV", against the flag you typed — rather than "Input validation failed".
    Point a command at an instance with --deevy-url, or set DEEVY_URL once.
    The Event stream is not a command yet.
  • cli — #73 3ec9144 Thanks @mattallty! - deevy events watch follows the Event log as it happens, resuming from where it got to and reconnecting when a stream ends — which...
Read more

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 08:59
Immutable release. Only release title and notes can be modified.
c129285

Minor Changes

  • core — #59 6d9a5b0 Thanks @mattallty! - A sub-issue may now live in a different Project from its parent. Work has dependencies that run across
    Projects — the piece that has to land in the API before the piece in the app can — and a parent link that
    stopped at the boundary did not remove the dependency, it moved it onto somebody writing it down twice. An
    Agent may open a child in any Project it was granted, and the child follows that Project's Workflow and its
    Gates. A parent in a Project you cannot see is not shown to you, and an Issue whose parent is hidden that way
    cannot be moved out of its tree.
    This also fixes an Issue's parent and children being labelled with the wrong key. They were named after the
    Project of the Issue you were looking at, which was invisible while a tree could not cross one.
  • core — #59 6d9a5b0 Thanks @mattallty! - Creating an Issue that is already assigned now tells the Assignee. It did not before — the assignment was
    silent until somebody changed it — so an Issue opened for a Member never reached their inbox, and one opened
    for an Agent never started its Run. Both do now, from the one call that created it. An Issue opened under a
    parent also says so in the Activity, rather than leaving a reader to click through to find out.
  • core, web — #59 6d9a5b0 Thanks @mattallty! - An Agent that splits work into sub-issues can no longer split it forever. Your Workspace now carries three
    limits — how many sub-issues one Issue may have, how many levels deep they may go, and how many may be open
    in one tree at once — and an Agent that reaches any of them is refused, with a line in the log so you can see
    that a number shaped the work rather than the Agent. They start at 20, 3 and 50, an admin changes them under
    Settings › Workspace, and they never apply to a Human.
  • core, web — #59 6d9a5b0 Thanks @mattallty! - An Issue now shows what its sub-issues are doing: where each one has got to, which ones have an Agent working
    them right now, and how many are still open. A Gate on an Issue whose sub-issues are unfinished says so where
    the buttons are — it does not stop you ruling, it just stops you being the last to know.
    And an Agent splitting work up no longer floods its Sponsor's inbox. Six sub-issues opened at once are one
    line, not six, and there is a second line when the last of them is finished. A sub-issue assigned to a Human
    still reaches that Human directly, because a rollup is for the wave and never for somebody's own work.
    "Sub-issues" is a Notification kind of its own now, so you can turn it off or route it to Slack like any other.
  • core — #59 6d9a5b0 Thanks @mattallty! - An Agent that splits work into sub-issues no longer has to sit and wait for them. It finishes its Run, and
    when the last sub-issue closes it gets a new one to pick the work back up from — reading what its sub-issues
    concluded off the Issue, the way anybody else would. Where there is no Agent left to wake, because it was
    suspended or lost the Project, the Issue still says its sub-issues are done, so the work is visibly somebody's
    rather than silently nobody's. A sub-issue in another Project counts exactly the same: done is done wherever
    it is.

Patch Changes

  • core, db — #59 6d9a5b0 Thanks @mattallty! - Two sub-issues of one parent finishing at the same moment no longer starts the parent's Agent twice. "At most
    one open Run per Issue and Agent" was a rule every caller checked for itself, which only held while nothing
    happened at once — and the ordinary ending of a fan-out is two things happening at once. The database keeps
    the rule now. If your instance already has duplicate open Runs from before this, the migration closes the
    later one and says so in its summary.
    Opening a sub-issue also got cheaper: it was spending more statements than a Cloudflare D1 request allows on
    the deepest tree the default limits permit, so on the Worker deployment it would have failed outright. Raising
    the "Levels deep" limit no longer makes that write more expensive at all.
  • db — #59 6d9a5b0 Thanks @mattallty! - An Agent that splits work into sub-issues can no longer split it forever. Your Workspace now carries three
    limits — how many sub-issues one Issue may have, how many levels deep they may go, and how many may be open
    in one tree at once — and an Agent that reaches any of them is refused, with a line in the log so you can see
    that a number shaped the work rather than the Agent. They start at 20, 3 and 50, an admin changes them under
    Settings › Workspace, and they never apply to a Human.

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 13 Sep 12:06
Immutable release. Only release title and notes can be modified.
b023621

Minor Changes

  • core, db, web — #57 3920978 Thanks @mattallty! - A Gate that wants two Humans no longer collects approvals of two different texts. When a Document changes
    while its Issue is sitting at such a Gate, the approvals already given are cleared and the Gate asks again —
    the ruling card says so where the buttons are, and the Activity says which Document changed. Nothing is
    deleted: the rulings stay in the log and the version each one approved stays pinned. A Gate that wanted one
    Human is untouched, because the Issue has already gone through it.
  • core, db — #57 3920978 Thanks @mattallty! - A room now remembers what was typed in it and turns it into versions. Thirty seconds of quiet writes the
    markdown the room holds; a further quiet within ten minutes, by the same people, amends that version rather
    than adding another, so an afternoon of writing leaves a history somebody can read. A version names everyone
    whose keystrokes are in it, and documents.versions reports them. A version a Gate ruled on is never
    amended. An Issue's description is saved the same way, without versions, because it has none.
  • core, web — #58 158a005 Thanks @mattallty! - A tab left open on a Document and put to sleep now comes back cleanly. A room whose stored state has grown
    large enough to be swept up starts the Document's identity again, and a browser that was away across that
    used to sync its old copy in beside the new one — the same text, twice. The room now turns such a browser
    away instead, and the browser puts what you typed while you were gone back as markdown, merged the way an
    Agent's write is merged. Where your words and the room's changed the same lines, the room keeps what it has
    and the editor hands yours back for you to place, rather than dropping them or pasting them over somebody.
  • core — #57 3920978 Thanks @mattallty! - An Agent writing a Document somebody is typing in no longer has to win or lose. documents.get hands back
    the live text and an opaque basis; echo it on documents.write and the server replays what you changed
    onto what the Document says now, so your paragraph and theirs both survive. Where the two genuinely
    collide — the same lines on both sides — the write is refused, naming the section, and the Agent re-reads
    and tries again; the Human typing is never interrupted. documents.writeSection rewrites one heading's
    worth without sending the rest of the Document, which merges by construction and reads better in the log.
  • core, web — #55 85cfa76 Thanks @mattallty! - Edit a Document where you read it. The intent, spec and plan panes have no Edit button and no read mode any
    more: the text on screen is the editor, and leaving it — or ⌘Enter — writes a version, with a line that says
    whether the save landed. The byline names every Member who has written a version, Humans and Agents alike
    ("written by Ada and Planner"), and the older versions moved behind a ⋯ menu that opens the full history,
    reads any version and can restore one. A save that would land on top of somebody else's is now refused
    rather than quietly winning: documents.write takes the version the text was read from, and a new
    documents.versions operation lists who wrote what and when.
  • core, web — #57 3920978 Thanks @mattallty! - Documents are live. Open an Issue in two browsers and you are writing in the same text: what one types the
    other sees, with a caret in their colour showing where they are and their avatar beside the byline. Nobody
    presses save — the room writes a version when the typing stops. Lose the connection and you keep typing; it
    merges when you are back, and the editor says so rather than pretending. An Issue's description works the
    same way.
    Where a deployment has no rooms — a Worker without the Durable Object binding — every editor behaves exactly
    as it did before, and health.ping says which it is.
  • core, db, web — #55 85cfa76 Thanks @mattallty! - A Gate ruling now records what it ruled on. Every approval and rejection pins the version each of the
    Issue's Documents stood at, so "approved" is a statement about words that cannot change afterwards: the
    ruling reads "on spec v2", it says so when the text has been written since, and the Document's history marks
    the version a Gate approved or rejected. documents.versions reports those rulings per version, for Agents
    as well as Humans.
  • core, db, web — #48 2051571 Thanks @mattallty! - Repositories are gone. Registering one only ever decorated a Link with the repository's own name and cost a settings screen, a table, two Event kinds and three API operations to do it; nothing read the attribution back, and it was never what let an Agent push — where a Run pushes is the runtime's own configuration.
    What this changes for you: the repositories.list, repositories.create and repositories.delete operations and the Settings › Repositories screen no longer exist, a Link no longer carries a repository or repositoryId (its kind and ref are still derived from the URL exactly as before), and the repository.created and repository.deleted Events are no longer written. The migration drops the repository table and the issue_link.repository_id column; the Links themselves are kept. The Workspace set-up strip also drops its allowlist item, whose fix was the Allowlist row on the same page, so it now counts a Project, an Agent and a Channel.
  • core, web, server — #57 3920978 Thanks @mattallty! - Groundwork for Documents two Members can write at once: the room exists, and nothing uses it yet. Operators
    deploying the Worker should know that it now declares a Durable Object binding (ROOMS) and the
    nodejs_compat and new_module_registry compatibility flags — a deploy creates the object, and Durable
    Objects need a paid Workers plan. The Node deployment needs nothing extra and answers the same websocket on
    the port it already listens on.
  • core, web — #57 3920978 Thanks @mattallty! - A Document open in your browser now tells you when an Agent has written into it. An Agent never joins a
    room — its edit arrives as a block rather than as typing, so a cursor for it would be a lie — and instead
    the Document's header says "Planner just wrote this" for as long as the change is still a surprise. The
    write itself lands in the room as it happens, so the paragraphs change in front of you rather than the next
    time the page is loaded.
  • web — #55 85cfa76 Thanks @mattallty! - Formatting floats over what you selected instead of sitting above what you are reading. The strip of buttons
    above every Document, description and comment is gone; select some words and the same buttons appear beside
    them. Inserting a table or a divider is what the / menu is for, so those two moved there rather than being
    carried around by a toolbar all day.
  • web — #55 85cfa76 Thanks @mattallty! - The Issue peek can be dragged wider by its left edge, and remembers how wide you left it. It still opens at
    45% of the window and will not go under that, or under the 56rem its two-column layout needs; the edge is a
    proper splitter, so the arrow keys move it and Home puts it back. On a phone, where the peek is the whole
    screen, there is nothing to drag.
  • web — #55 85cfa76 Thanks @mattallty! - An Issue's title and description are edited where they are read, the way its Documents already were. The
    Edit button and the form behind it are gone: click the title and type, press Enter or move on and it is
    saved; Escape puts back what was there, and an empty title is refused rather than written. The description
    is the same editor a Document gets, and a line beside the Issue's key says when something is saving, when it
    is saved, and offers to...
Read more