Repository navigation
Releases
Releases · WeAreNendo/deevy
v0.11.4
Immutable
release. Only release title and notes can be modified.
Patch Changes
- hosted — #190
36bbe5dThanks @mattallty! - The hosted Worker's own/healthznow also answers withversionId, the id of the Cloudflare Worker version serving the request, through a newversion_metadatabinding (CF_VERSION_METADATA) in itswrangler.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
828f086Thanks @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/healthzalready 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
Immutable
release. Only release title and notes can be modified.
Patch Changes
- core, web — #173
2f3bc0dThanks @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-secretstill works. - core — #177
1472945Thanks @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.listandcheckpoints.setkeep each Checkpoint'sidacross saves. - core, web — #175
f50792eThanks @mattallty! - At a Checkpoint with Not the Human the work is for on, that Human can now reject the Gate, in deevy, with/rejecton 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) gainsyou.mayApprove,you.whyNotApproveandshortfall({ wanted, couldApprove, why }, or null).you.mayRuleandyou.whystill mean whether you may rule at all and why not, so for the excluded HumanmayRuleis now true andmayApprovefalse.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 hasloweredFrom, the old threshold, in the payload of itsgate.approvedandrun.answeredEvents, which webhooks deliver. - core — #172
709241dThanks @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 itdeevy 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 aproject.updatedEvent saying so, instead of routing records to an Agent that cannot work them. - core, web — #174
5324623Thanks @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 itdeevy allowlist add, now refuses such a rule withPRECONDITION_FAILEDand a message saying why, where it used to save it. - core, sockets, web — #178
0b43b06Thanks @mattallty! - A Socket's answers no longer carry its previous webhook secret, which everysockets.*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. Andsockets.rotatenow 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.providerssays which way each tool works, in a newwebhookSecretFromfield (either,toolorhandshake), and the Socket's page offers what that says. A Socket module of your own must now declarewebhookSecretFrom. - design, web — #185
f55482cThanks @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
88aa8bcThanks @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
54e7e04Thanks @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
5afa3c4Thanks @mattallty! - The repository'sdocker-compose.ymlnow asks forDEEVY_SECRET, as it already as...
v0.11.2
Immutable
release. Only release title and notes can be modified.
Patch Changes
- core — #164
4796865Thanks @mattallty! - The API reference at/api/docsworks 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
288dbe6Thanks @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
7526cb2Thanks @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.icoandapple-touch-icon.png. A proxy that lets only known paths through needs to allow them, the way it allowsfavicon.svg.
v0.11.1
Immutable
release. Only release title and notes can be modified.
Patch Changes
- hosted — #158
8c8a7a5Thanks @mattallty! - The hosted Worker's archive,deevy-hosted-<version>.tar.gzwith 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
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core, adapters, web — #142
9e267b9Thanks @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 aContent-Security-Policythat allows scripts from deevy's origin only (no inline script, noeval, nothing from a CDN) and lets nothing frame it, so a Gate's Approve cannot be clicked through someone else's site; beside it comeX-Frame-Options: DENY,X-Content-Type-Options: nosniffandReferrer-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_headersfile, which the asset handler applies./api/docsloads 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.
APOST,PUT,PATCHorDELETEto/rpcor/apithat your session cookie signed in is refused with403unless the browser says it came from the same origin (Sec-Fetch-Site: same-origin), or sends anOriginofBETTER_AUTH_URLorDEEVY_WEB_ORIGIN(with noBETTER_AUTH_URL, the address the request reached). A proxy that strips both headers or rewritesOriginwill make every change in the app fail, and an SPA served from an origin of its own needs that origin inDEEVY_WEB_ORIGIN, as it already did. Agents' API keys and CLI or MCP access tokens are unaffected, and/mcpno longer accepts a browser session at all, only a bearer.
A sign-in provider's access and refresh tokens are now encrypted at rest underBETTER_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 anhttporhttpsaddress, 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
4544e95Thanks @mattallty! - Settings shows the version deevy runs and links to its source; /healthz reports the version. The foot of every Settings page readsdeevy <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./healthznow answers{"ok":true,"version":"<version>"}, andhealth.pinghas aversionfield; both saynullwhen the build did not name one. A probe that only checksokor the status code needs no change. - web — #150
c2ad0ecThanks @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.pingsays which a deployment offers in a newlivefield:streamfor the Docker image and a Worker of your own, which work as before, andwebsocketfor a hosted Workspace. A tab that cannot open the socket, behind a proxy that drops WebSocket upgrades say, uses the stream instead. - server — #138
8b8cb4dThanks @mattallty! - deevy can live under a path of its host, such ashttps://company.example.com/deevybehind a reverse proxy: setBETTER_AUTH_URLto 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 setsassets.run_worker_firsttotrue; 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
4f81af8Thanks @mattallty! - The many-Workspaces Worker limits what each Workspace may do in a day, since they all send through one sender: pastDEEVY_HOSTED_INVITATIONS_PER_DAYinvitations in the last 24 hours (50 by default) the next is refused with when it can be made, and pastDEEVY_HOSTED_EMAILS_PER_DAYemails (500) what is owed waits and goes out as the window opens, which Settings › Email shows.Platform.configuregives one Workspace limits of its own, andPlatform.statusreports 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
41b97b6Thanks @mattallty! - Every release now carries the many-Workspaces Worker ready to deploy:deevy-hosted-<version>.tar.gzon the GitHub Release, with its.sha256and 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, awrangler.jsonwith what only the deployer knows left as marked blanks (<<HOST>>,<<DIRECTORY_KV_ID>>,<<CONSOLE_SERVICE>>), and amanifest.jsonnaming 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'sPlatform.statusreports the version its build was made from. See "The many-Workspaces Worker" in OPERATIONS.md. - server — #145
2df3151Thanks @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 aPlatformservice 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
e5b97a1Thanks @mattallty! - Several deevy deployments can share one set of sign-in provider Apps through a relay: setDEEVY_SIGN_IN_RELAY_SECRETon all of them,DEEVY_SIGN_IN_RELAY_URLon each deployment that signs in through the relay, andDEEVY_SIGN_IN_RELAY_ALLOWon 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
3b0b7f6Thanks @mattallty! - A hosted Workspace can be taken home. The image carriesdist/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, checksDEEVY_SECRETagainst 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 theBETTER_AUTH_SECRETandDEEVY_SECRETthe export handed over.dist/import.mjs --for-d1turns 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
6692d80Thanks @mattallty! - Every release from this one on upgrades in place, and can be rolle...
v0.10.0
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core, db, web, server — #125
52dd90aThanks @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 withBETTER_AUTH_SECRETand works for six months.
The migration adds a nullableemailcolumn tonotification_preference, applied at startup on Docker. On Workers, apply the D1 migrations before deploying, as for every release. - core, email, web, server — #124
2494adfThanks @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. SetDEEVY_EMAIL_SENDER=resend,DEEVY_EMAIL_FROM(an address on a domain Resend has verified, such asdeevy <deevy@yourcompany.com>) andRESEND_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 asemail.exhausted, in the sender's own words. With no sender configured, nothing is queued up for later. - core, db, web — #131
2c29f02Thanks @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. Passsend: falsetoinvitations.createto only get the link. To email it later, the link's token is kept sealed underDEEVY_SECRET, and only until the email lands or the invitation is accepted, revoked or expires; withoutDEEVY_SECRETthe invitation is not emailed, and the dialog says so. A migration addsinvitation.sealed_token, which no read ever returns. - core, db, web, server — #128
29c750eThanks @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 underDEEVY_SECRETlike 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 isemail.status,email.configure,email.clearandemail.test, and the Event log recordsemail.configuredandemail.cleared(without the key). A migration adds theemail_sendertable. - core, db, web, server — #127
eba859aThanks @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 ischannels.createEmail, and every Channel now reportsaddressandconfirmedAt. - adapters, email, web, server — #130
066de9dThanks @mattallty! - Two more ways to send email, each where it can run.- SMTP, on the Docker image. Set
DEEVY_EMAIL_SENDER=smtpandSMTP_URL, such assmtps://user:password@mail.example.com:465, orsmtp://…: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_emailbinding namedEMAILinwrangler.jsoncand setDEEVY_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.
- SMTP, on the Docker image. Set
- email, web — #129
a782f32Thanks @mattallty! - deevy can now send email through Postmark, SendGrid, Mailgun or Amazon SES as well as Resend, on either deployment. SetDEEVY_EMAIL_SENDERtopostmark,sendgrid,mailgunorses, or choose one under Settings › Email:- Postmark:
POSTMARK_SERVER_TOKEN, and optionallyPOSTMARK_MESSAGE_STREAM(defaultoutbound). - SendGrid:
SENDGRID_API_KEY. - Mailgun:
MAILGUN_API_KEYandMAILGUN_DOMAIN, andMAILGUN_REGION=eufor a domain in Mailgun's EU region. - Amazon SES:
AWS_SES_REGION,AWS_SES_ACCESS_KEY_IDandAWS_SES_SECRET_ACCESS_KEY, ideally for an IAM key that mayses:SendEmailand 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.
- Postmark:
Patch Changes
- server — #122
7edb623Thanks @mattallty! - The server image runs on a rebuilt base with OpenSSL's October fixes (libssl3t643.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
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core, web — #112
74f2381Thanks @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 newagents.usageoperation (GET /agents/{memberId}/usage, up to twelve calendar months in UTC, counting each Run in the month it was created).
- core, sockets — #118
2425e82Thanks @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
480d048Thanks @mattallty! - deevy can now offer Microsoft, Linear, Slack and Atlassian sign-in, beside GitHub, Google, GitLab and your own OpenID Connect IdP. SetMICROSOFT_CLIENT_IDandMICROSOFT_CLIENT_SECRET(andMICROSOFT_TENANT_IDto narrow the accounts it takes:organizations, or your tenant's id),LINEAR_,SLACK_orATLASSIAN_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_domainrule admits a Slack sign-in. - Microsoft does only when the tenant sends the optional
verified_primary_emailandverified_secondary_emailclaims;docs/OPERATIONS.mdsays 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_ORDERputs the ids you name first, e.g.oidc,githubfor 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.
- Slack does, so an
- core, db, web — #106
8088cf2Thanks @mattallty! - A Run can now say what it spent. Whatever runs an Agent reports what one session of a Run spent withruns.reportUsage(POST /runs/{runId}/usage, and the MCP toolruns_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.getandruns.listcarry the Run's totals asusage, andruns.getbreaks 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, andunpricedTokenssays how much of a Run no cost covers. The Event log records each report asrun.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
0e3d48fThanks @mattallty! - Every Run now says how long it took. Each Run in the API carriestiming: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; andworkingMs, 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 onrunare 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
449cd6fThanks @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.
SetDEEVY_SECRETto at least 32 random characters before connecting anything: every tool's credential is sealed under it. Keep it apart fromBETTER_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 andDEV-42keys, 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 andnodejs_compatis off.
How a record reaches an Agent. A tool delivers toPOST /hooks/<socketId>, signed. A labelagent:<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 carriesdeevy:awaiting-approvalwhile it waits, and the ruling is another comment with its arithmetic ("1 of 2"). With the Project's Mirror set toruns, Runs starting and finishing are said too;offsays 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=1registers 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 indocs/plans/sockets.md, and the reasoning is ADR-0024 and ADR-0025. - core, sockets, web — #104
449cd6fThanks @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.pemwhole.
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/approveor/reject <why>, with no linking step.
DEEVY_GITHUB_APIpoints 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" indocs/OPERATIONS.mdwalks through it. It was walked end to end against github.com, with Claude Code as the Agent. - core, sockets — #104
449cd6fThanks...
v0.8.0
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core, cli — #71
a6c699cThanks @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` andhttps://hostare two instances and were sharing one file. Signing in again is all that is needed. - core — #67
111803fThanks @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/mcpand 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 withinvalid_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-serverand 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
c8deadbThanks @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, noapt, 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:If you forget, deevy tells you: it checks that it can writedocker run --rm -v deevy-data:/data alpine chown -R 65532:65532 /data
/databefore 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 tohealthyand 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, sochown -R 65532:65532that before the first start.
The image now carries its own healthcheck, sodocker psreportshealthyand Compose'sdepends_on: condition: service_healthyworks without a healthcheck of your own, on whateverDEEVY_PORTyou set. The one in the publisheddocker-compose.ymlhas been removed. If you have a copy of the old file, delete thehealthcheck:block under thedeevyservice — it callsnodeby name, and the new image has no shell and noPATHentry to find it with, so it would reportunhealthyforever and hold back anything waiting on it.
Backups are unchanged:sqlite3was never in this image and still is not, so the sidecar recipes indocs/OPERATIONS.mdare 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--useror auser:in Compose, it still works; that is now the default rather than something you have to ask for. - cli — #66
5ba116fThanks @mattallty! - The first piece of a deevy CLI: a new@deevy/clipackage 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
5825952Thanks @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, sodeevy inbox listshows you what is unread anddeevy members listshows 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.
--jsonis 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, andNO_COLORturns 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
a142f03Thanks @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.--jsonis on every command.
Four operations answer with an explanation instead of a request.gates approveandgates rejectare a Human's, in a browser, and no CLI, key or Agent can rule a Gate whatever it is signed in as.oauth-clients listandoauth-clients revokewant 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 bareFORBIDDEN.
It also refuses before asking when it already knows the answer: an operation an Agent may not call says thatDEEVY_API_KEYis 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 setDEEVY_URLonce.
The Event stream is not a command yet. - cli — #73
3ec9144Thanks @mattallty! -deevy events watchfollows the Event log as it happens, resuming from where it got to and reconnecting when a stream ends — which...
v0.7.0
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core — #59
6d9a5b0Thanks @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
6d9a5b0Thanks @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
6d9a5b0Thanks @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
6d9a5b0Thanks @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
6d9a5b0Thanks @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
6d9a5b0Thanks @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
6d9a5b0Thanks @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
Immutable
release. Only release title and notes can be modified.
Minor Changes
- core, db, web — #57
3920978Thanks @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
3920978Thanks @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, anddocuments.versionsreports 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
158a005Thanks @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
3920978Thanks @mattallty! - An Agent writing a Document somebody is typing in no longer has to win or lose.documents.gethands back
the live text and an opaquebasis; echo it ondocuments.writeand 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.writeSectionrewrites one heading's
worth without sending the rest of the Document, which merges by construction and reads better in the log. - core, web — #55
85cfa76Thanks @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.writetakes the version the text was read from, and a new
documents.versionsoperation lists who wrote what and when. - core, web — #57
3920978Thanks @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, andhealth.pingsays which it is. - core, db, web — #55
85cfa76Thanks @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.versionsreports those rulings per version, for Agents
as well as Humans. - core, db, web — #48
2051571Thanks @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: therepositories.list,repositories.createandrepositories.deleteoperations and the Settings › Repositories screen no longer exist, a Link no longer carries arepositoryorrepositoryId(its kind and ref are still derived from the URL exactly as before), and therepository.createdandrepository.deletedEvents are no longer written. The migration drops therepositorytable and theissue_link.repository_idcolumn; 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
3920978Thanks @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_compatandnew_module_registrycompatibility 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
3920978Thanks @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
85cfa76Thanks @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
85cfa76Thanks @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
85cfa76Thanks @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...