Skip to content
Back to home

Release history

Follow the full releases on GitHub .

v0.40.8

Aug 23, 2026 · v0.40.8

v0.40.8 — Patch: Webhook delivery failures surface to operators

Two small hardening items from the final review of the outbound-fetch wave.

Broken remote webhooks are no longer silent

A failed remote webhook delivery — network error, validation rejection, timeout — previously wrote a failed delivery row and moved on. Nothing told the operator: no event, no notification, no visible signal until someone manually inspected delivery rows. Delivery failures now publish a webhook.delivery_failed event (rendered as a warning toast in the UI) and send an in-app notification to habitat admins with the endpoint URL and the failure reason. A broken endpoint surfaces the moment it breaks.

Jira connections tightened to port 443

The Jira Cloud known-good check accepted any explicit port on a tenant host (e.g. :8443) — safe against credential capture but broader than the Jira API surface. Only port 443 (or no explicit port) is now accepted.

No operator action required

No configuration changes needed. The Jira port tightening only affects connections that explicitly configured a non-443 port on an Atlassian tenant (none are known to exist in practice).

Verification

  • @orcy/api suite: 414 files, 6,543 passed / 2 skipped / 0 failed. @orcy/mcp suite: 38 files, 706 passed. @orcy/ui build: clean. corepack pnpm lint: 0 errors.
  • The delivery-failure test proves both the SSE event and the failed-delivery row for a DNS-rejecting endpoint; the port-tightening tests pin :443 accepted, other ports rejected.

v0.40.7

Aug 23, 2026 · v0.40.7

v0.40.7 — Patch: Jira Cloud-only and every outbound fetch validated and pinned

This patch closes the last two items from the outbound-security backlog — both found by the adversarial review pass that audited the v0.40.6 hardening — and it makes one user-visible rule change for Jira.

If you connect Jira, read this first. API-key Jira connections now accept Jira Cloud tenant URLs onlyhttps://<tenant>.atlassian.net. Any other address (self-hosted Jira, an internal host, an http URL) is rejected at configuration time with a clear message. Existing connections to other addresses stop syncing and surface the reconnect instruction in the connection's sync status. Jira ticket work in Orcy — importing, claiming, commenting, closing — is otherwise unchanged. Jira OAuth connections (which already talk only to Atlassian's fixed API host) are unaffected.

Jira connections can no longer point anywhere

The API-key connection previously accepted any URL typed into the site field, and the sync adapter then fetched that address with the team's Jira credential attached — meaning an internal-network address would be probed from inside your network, and a hostile public address would receive your credential. Rather than bolt on an allowlist, the surface now has a structural known-good rule: because only Atlassian-owned tenant hosts can ever be configured, every credentialed request provably goes to Atlassian's own servers — no allowlist to maintain and no way to aim the credential at anyone else. Syncs additionally go through the validated, DNS-pinned, redirect-fail-closed fetch helper introduced in v0.40.6.

Every remaining webhook-class fetch is pinned

Outgoing-webhook delivery (including test sends), Slack, and Discord posts previously validated a URL and then fetched it unpinned — a second DNS lookup at fetch time, plus undici's default redirect-following, reopened both the rebinding and redirect bypasses the earlier patches closed elsewhere. All three now use the validated, pinned, fail-closed helper, and chat integrations inherit the fix by delegation. Two behavior notes: webhook endpoints that respond with a redirect now fail (configure the final URL), and a security-doc statement about which surfaces pin is now literally true of the full set. A pre-release adversarial review of this patch ran an independent bypass hunt over the Jira host rule (userinfo, trailing dots, sub-domains, punycode, ports — all hold) and confirmed no unvalidated outbound fetch remains anywhere in the API.

Verification

  • @orcy/api typecheck and build: clean. @orcy/mcp suite: 38 files, 706 passed. corepack pnpm lint: 0 errors.
  • @orcy/api suite: 414 files, 6,542 passed / 2 skipped / 0 failed.
  • Fix tests were authored first and proven discriminating (route and adapter rejections fail without the guard; a removed-dispatcher mutation fails the pinning tests). Adversarial-review findings were remediated before release; two deliberate non-goals are recorded in the issue threads (no allowlist, no auth escalation — the structural rule replaces both).

v0.40.6

Aug 23, 2026 · v0.40.6

v0.40.6 — Patch: Outbound-fetch hardening and latent plugin watchdogs

If you operate webhooks or notification channels, read this first. This patch closes the DNS-rebinding window in outbound URL validation and extends validation to three previously-unchecked paths. Operator-visible changes: (1) hostnames that return no DNS answer are now rejected — previously they passed as valid, so a webhook target that stops resolving will now fail validation (400 at configuration time, a recorded failed delivery at dispatch time); (2) the notification webhook channel now rejects internal/LAN targets at delivery time — operators pointing it at a deliberately internal destination (self-hosted ntfy, a local bridge) must list the hostname in ORCYSSRFALLOWLIST; (3) remote webhook endpoints whose hostnames resolve to private space now 400 at create/PATCH — existing stored endpoints keep listing and enabling, and simply fail per dispatch with a recorded failed-delivery row.

Outbound fetches are pinned to their validated resolution

The canonical URL checker resolved DNS, returned a verdict, and every caller then fetched by hostname — resolving DNS a second time. A rebinding hostname (public answer at validation, private at fetch) bypassed the private-IP block; worse, a hostname that returned no DNS answer passed as valid. The checker now fails closed on empty answer sets, returns its resolved addresses, and a new shared helper validates and fetches pinned to exactly those addresses via a custom connection lookup — one DNS resolution total, TLS/SNI still bound to the hostname (verified down to the connector), fail-closed redirects, and a ten-second timeout that now also bounds response-body reads. The plugin webhook capability and automation call_webhook actions use the helper, and a previously committed test that pinned the fail-open behavior is rewritten to the closed contract.

Three previously-unvalidated paths now use the canonical checker

Remote webhook registration validated URLs with a hostname-prefix list (no resolution, no IPv4-mapped handling); the compact remote webhook dispatcher fetched stored endpoints with no dispatch-time check at all; and the notification webhook channel had no validation anywhere. All three now validate through the canonical checker — remote webhooks at configuration and dispatch time, the notification channel at delivery time (it has no configuration surface) — pinned and fail-closed like the rest. SECURITY.md's coverage statement names exactly which paths pin.

Two latent plugin watchdogs

Notification channels and post-interceptors kept the zero timeout default that disables the invocation watchdog — the same never-settling hang class fixed for automation actions in v0.40.4. Both now default to 30 seconds. This is latent today (channel delivery is not yet wired into production notification dispatch, and post-interceptors are fire-and-forget), and quarantine accounting is unchanged per the invocation policy: the watchdog terminates and faults the run; faults still do not count toward quarantine for these kinds. A manifest-declared timeoutMs: 0 remains an explicit opt-out for both.

Verification, including an adversarial review

Before release, an independent three-verifier review pass attacked the changes. It confirmed SNI/certificate validation cannot be silently re-targeted under pinned lookup and that the connection-pool eviction cannot break in-flight fetches; its findings are all addressed here or tracked: a dead statement and an over-broad doc claim were fixed in-tree, and two follow-ups are filed (four remaining validate-then-unpinned-fetch surfaces — outgoing-webhook delivery, Slack, Discord, chat — and the Jira adapter's member-supplied base URL, which ships unvalidated and is the highest-priority item in the backlog).

  • @orcy/api typecheck and build: clean. @orcy/mcp suite: 38 files, 706 passed. corepack pnpm lint: 0 errors.
  • @orcy/api suite: 414 files, 6,535 passed / 2 skipped / 0 failed.
  • Fix tests were authored first and proven discriminating via mutate-and-revert; the rebind pinning, empty-DNS fail-closed, literal-IP, and per-path rejection tests all fail against the unfixed code.

v0.40.5

Aug 23, 2026 · v0.40.5

v0.40.5 — Patch: Committed pre-push gate and corrected route docs

A follow-up pass over the hardening wave (a subagent verification of the remediation's own discoveries) filed five tracked items; this patch ships the two that change no product behavior. The remaining three — the outbound-fetch SSRF hardening pair and two latent plugin watchdogs — ship together as the next patch so the only behavior-tightening changes stay isolated.

The pre-push migration gate is now installable from the repo

The first layer of the production-migration safety — the local pre-push hook that blocks a failing main push — existed only on machines where it had been placed by hand; nothing installed it, so fresh clones silently ran with post-push CI verification only. The hook's source is now committed at hooks/pre-push and installs with one idempotent command (bash scripts/install-hooks.sh, documented in CONTRIBUTING). The committed hook invokes the gate through corepack, and CONTRIBUTING's gate commands are corepack-prefixed too: its on-demand snippet previously used bare pnpm, the exact form that blocked the v0.40.3 release push when an ambient package-manager shim refused the pinned project under a spawned shell. That trap is now documented in the hook itself.

Route comments match the wire again

Seven comments across the habitats, board-export, and board-analytics routes still claimed board response keys that no handler has returned since the v0.36.0 Public Habitat Transport rename. Comments only; no behavior change.

No operator action required

Developer tooling and documentation only. No schema changes, no API behavior changes, no configuration.

Verification

  • @orcy/api typecheck and build: clean. @orcy/mcp build: clean. corepack pnpm lint: 0 errors.
  • @orcy/api suite: 414 files, 6,519 passed / 2 skipped / 0 failed. @orcy/mcp suite: 38 files, 706 passed / 0 failed.
  • The hook installer is proven idempotent and byte-identical on this machine; the installed gate ran during this release's push.

v0.40.4

Aug 22, 2026 · v0.40.4

v0.40.4 — Patch: Invocation watchdog and canonical webhook SSRF validation

If you operate webhooks or long-running automation actions, read this first. This patch is deliberately behavior-tightening, shipped alone so it is easy to bisect or roll back: (1) webhook fetches — plugin webhook calls and automation call_webhook actions — now fail closed on redirects and after 10 seconds; a webhook endpoint that answers with a redirect or takes longer than 10s will start failing loudly. (2) Automation-action plugin invocations now have a 30-second default watchdog; an action that legitimately needs longer must declare an explicit timeoutMs (a declared timeoutMs: 0 remains a valid opt-out that disables the watchdog).

Both changes come from the second half of the adversarial audit filed at v0.40.2 (the first half shipped in v0.40.3). No configuration or migration is otherwise required.

Automation actions can no longer hang forever

The invocation policy gave automation actions no default timeout, and a timeout of zero disables the invocation watchdog entirely — so an automation-action handler that never settled held its invocation open indefinitely and, because neither fault-accounting path was reachable without a rejection or timeout, never counted toward quarantine despite the policy saying it should. The kind default is now 30 seconds (network-bound, versus the five-second compute-bound detector default): a hung action is terminated, its plugin run is marked failed, and the fault increments the quarantine counter like any other runtime fault. The watchdog ends the run — it is not cancellation, per the existing runtime contract. A manifest-declared timeoutMs: 0 is unchanged as an explicit opt-out, and the distinction between declaring zero and omitting the field is now pinned by tests through the production dispatch path.

Webhook URLs are checked by the canonical SSRF guard

Plugin webhook calls and automation call_webhook actions validated their URLs with a copy-pasted regex over the raw URL string, while the canonical validateOutboundUrl checker — which resolves the hostname and rejects private-space answers — guarded every other outbound path in the product. Both webhook paths now use the canonical checker and the duplicated pattern lists are gone, closing the reproduced bypass classes: hostnames that resolve to private addresses, and IPv4-mapped IPv6 literals, which the canonical checker now also rejects (it previously missed them too — both dotted ::ffff:127.0.0.1 and hex ::ffff:7f00:1 forms are unwrapped and checked as IPv4). Webhook fetches also gain a ten-second timeout and fail-closed redirect handling, so a redirecting or stalling endpoint can no longer sidestep the URL check mid-flight.

No other operator action required

Apart from the callout above, behavior is strictly hardening. Webhook endpoints that neither redirect nor stall are unaffected; URLs that were already valid remain valid. No schema changes.

Verification

  • @orcy/api typecheck and build: clean. corepack pnpm lint: 0 errors.
  • @orcy/api suite: 414 files, 6,519 passed / 2 skipped / 0 failed.
  • Fix tests were authored first and proven discriminating: the watchdog test hangs (reproducing the finding) against the old policy, and removing the IPv4-mapped unwrap fails the mapped-literal tests. The opt-out and DNS-rebind rejections are pinned on both webhook paths with offline-reproducible dns mocks.