Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

OS Notifications Agent

Forwards OS-level notifications (Windows Action Center, Linux freedesktop) into TypeAgent chat as ephemeral toasts/inline messages. macOS is not supported.

The agent is off by default and must be enabled explicitly:

@config agent enable osNotifications

Lifecycle

The watcher lives in startBackgroundTasks / stopBackgroundTasks rather than updateAgentContext. Even though the agent now has actions (and so updateAgentContext would fire when actions are enabled), the watcher is conceptually a session-scoped background task — exactly what startBackgroundTasks is for, per the SDK comment.

@config agent enable osNotifications
        │
        ▼   AgentToggle.Agent flips schemas/actions/commands
        │
[appAgentManager.setState — commands enable]
        │
        ▼
[ensureSessionContext]
        │
        ▼
initializeAgentContext()        ← returns empty agent context (with ChoiceManager)
        │
        ▼
startBackgroundTasks(ctx)       ← watcher starts, attached to ctx.agentContext
        │
        ▼
   (running) — watcher callback emits via context.notify(...)
        │
@config agent disable osNotifications
        │
        ▼
[closeSessionContext]
        │
        ▼
stopBackgroundTasks(ctx)        ← watcher.stop() awaited first, no notify races
        │
        ▼
closeAgentContext(ctx)          ← (not implemented — no extra cleanup needed)

How it works

[per-OS watcher]                                [chat clients]
     │  added/removed events                          ▲
     ▼                                                │
[osNotifications agent] ── sessionContext.notify ──── │
     - app filter                                     │
     - rate limit                                     │ broadcast
     - "new only" timestamp gate                      │ (every connected
     - dismiss tracking                               │  client)
                                                      │
                                          [shell renderer / CLI]
                                          - chat bubble (toast / inline / info)
                                          - removed on osDismiss
                                          - NOT recorded in @notify show
                                          - NOT persisted to DisplayLog

The agent uses sessionContext.notify(...) with no persist flag, so notifications never enter the displayLog.json. Lifecycle is bounded by the OS notification center: when the OS reports a notification has been dismissed, the agent emits an osDismiss event and the chat bubble is removed from the DOM.

Diagnostic actions and commands

Two diagnostic operations exposed both as actions (natural-language) and as @osNotifications subcommands. Both forms drive the same code path — the action handler is the source of truth, the commands are thin wrappers.

Action NL examples Command What it does
syncOsNotifications "sync os notifications", "show my notifications", "replay system notifications" @osNotifications sync Re-emits currently-present notifications through the agent pipeline (filters, rate limit, dismiss tracking apply).
testOsNotification "test notification saying hi", "send me a test notification with foo" @osNotifications test "msg" Synthesizes an OsNotificationAdded event and feeds it into onWatcherEvent — verifies the agent end-to-end with no real OS source.

Sync and setup

syncOsNotifications is Windows-only (Linux's freedesktop spec doesn't expose existing notifications, only new ones — the action surfaces a clear warning instead).

On Windows, the agent participates in the dispatcher's readiness/setup framework:

  • checkReadinesssetup-required when OsNotificationListener.exe isn't present, ready otherwise.
  • setup → returns the in-chat yes/no card asking whether to build the helper now. Click "Yes" and buildAndRetrySync runs the full WinAppSDK build/sign/register pipeline (see "Per-platform setup → Windows" below), then restarts the watcher.

Pre-flight at the dispatcher level means @osNotifications sync (or any NL form like "sync os notifications") on a system without the helper is blocked before the agent's code runs and the user is pointed at:

@config agent setup osNotifications

That command surfaces the build prompt. The choice card is wired via the standard ChoiceManager + handleChoice pattern (ChoiceManager lives on AgentContext; the AppAgent's handleChoice delegates back to it). A build mutex (AgentContext.buildInProgress) protects against two clients each clicking "Yes" on their own card before either build completes.

Commands sharing the action code path

@osNotifications sync and @osNotifications test live alongside the actions because some users prefer explicit command invocation over conversational form. Each command builds the corresponding TypeAgentAction object and returns the result of the same performSync / performTest helper the action handler uses.

This works because command handlers can now opt in to returning an ActionResult (signature: Promise<ActionResult | undefined | void>). The dispatcher's command pipeline runs the same post-execution processing as the action pipeline — display content, pendingChoice (in-chat yes/no card), dynamicDisplayId — so command and NL invocations render identically. Returning void keeps the legacy "use actionIO directly" pattern; both are supported.

Configuration

Defaults live in src/osNotificationsConfig.ts:

Field Default Notes
mode "toast" toast / inline / info — maps to AppAgentEvent.{Toast,Inline,Info}
routing "broadcast" broadcast to every client; currentOnly is a TODO (see below)
redactBody false When true, drop body and forward title + app only
bodyMaxChars 200 Body truncated with past this length
allowList (unset) When set + non-empty, only these app names pass (case-insensitive equality)
blockList (unset) Drop matching apps. Ignored if allowList is set
maxPerMinute 20 Rolling 60s window — excess notifications are silently dropped

Privacy note. Notification bodies often contain message previews, calendar event titles, and 2FA codes. We do not filter 2FA — the user usually needs to see those. If you want stricter privacy, set redactBody: true or use allowList to forward only specific apps.

Per-platform setup

Windows

The Windows watcher spawns a small .NET console exe that subscribes to Windows.UI.Notifications.Management.UserNotificationListener and streams events as JSON-per-line on stdout.

The exe needs package identity to subscribe to NotificationChanged. That's the API that gives us realtime events (no polling, no 3-second delay). On a plain unpackaged exe the subscription silently fails. We solve this with a WinAppSDK sparse package: a tiny MSIX containing only an AppxManifest.xml that declares the identity + userNotificationListener capability, signed with our dev cert, and registered against the unpackaged exe via Add-AppxPackage -ExternalLocation. The exe carries a side-by-side <msix> element in its app.manifest linking it to the identity package.

One-time prereqs:

  1. Pull the dev code-signing cert from Azure Key Vault — the dev cert is held in the aisystems Key Vault as TypeAgent-Development-Certificate (with its PFX password as a sibling secret). The included getCert.mjs tool wraps the download:

    # Pull cert + password to %TEMP%\TypeAgent-Development-Certificate.pfx
    node ts/tools/scripts/getCert.mjs pull
  2. Install the cert into both CurrentUser and LocalMachine stores. AppX deployment runs in SYSTEM context and only honors LocalMachine certs, so the cert must live there for Add-AppxPackage to accept the MSIX signature. SignTool itself uses CurrentUser. Use the helper in elevated PowerShell:

    node ts/tools/scripts/getCert.mjs install --trusted-root

    install puts the cert in CurrentUser\My, CurrentUser\TrustedPeople, LocalMachine\My, LocalMachine\TrustedPeople. --trusted-root additionally adds it to LocalMachine\Root so AppX deployment trusts the chain (interactive — Windows will prompt for consent on the root install).

  3. Cert renewal. If signtool fails with "no Code Signing EKU found", the cert needs a new version with the right EKU:

    node ts/tools/scripts/getCert.mjs renew
    node ts/tools/scripts/getCert.mjs install --trusted-root

Build the helper:

The agent's setup hook does this end-to-end — clean + publish the exe, pack the identity manifest, sign the MSIX, register the sparse package. From chat:

@config agent setup osNotifications

The yes/no card spells out what's about to happen. Build progress streams inline.

For manual builds (debugging, CI):

cd dotnet/osNotificationListener
dotnet publish -c Release -r win-x64 -o publish

# pack identity MSIX
makeappx pack /o /d identity /nv /p TypeAgent.OsNotificationListener.msix
signtool sign /sha1 <thumbprint> /fd SHA256 TypeAgent.OsNotificationListener.msix

# register sparse package against the unpackaged exe
Add-AppxPackage -Path TypeAgent.OsNotificationListener.msix -ExternalLocation publish

The exe ends up in dotnet/osNotificationListener/publish/OsNotificationListener.exe. The TypeScript watcher locates it via import.meta.url — checks publish/ first, falls back to the project root for legacy hand-built copies.

See ../../../../dotnet/osNotificationListener/README.md for the C# helper details.

Linux

In-process via dbus-next. Eavesdrops on org.freedesktop.Notifications.Notify calls on the session bus. No build step required.

The agent surfaces a one-time warning if AddMatch fails — typically because the bus policy denies eavesdropping. On most user desktops this works out of the box.

macOS

Unsupported. Apple does not expose other apps' notifications via a public API. The agent emits a one-time info notification explaining this and otherwise idles.

Caveats

Windows: package identity

UserNotificationListener works from unpackaged hosts on most modern builds, but its NotificationChanged event (the one that gives us realtime updates instead of polling) requires package identity. The sparse-package setup above gives the helper exe identity without bundling it as a full MSIX app — the WinAppSDK side-by-side manifest (<msix> element pointing at TypeAgent.OsNotificationListener in the exe's app.manifest) tells Windows where to find the identity package at runtime.

If something is wrong with the sparse-package setup (missing cert trust, expired signature, registration failed), the helper emits a kind:"error" event with details and the agent surfaces it once. Run @config agent refresh osNotifications after fixing the underlying issue.

Windows: first-run consent

Windows shows a system-level prompt the first time RequestAccessAsync is called. The user can revoke consent later from Settings → Privacy & security → Notifications. The helper exits with code 2 if access is denied.

Windows: cert trust store split

Add-AppxPackage runs the signature check from a SYSTEM-context process and only consults LocalMachine cert stores. SignTool, which runs as the user, only consults CurrentUser. The cert must therefore live in both trees, and the chain has to terminate in LocalMachine\Root for AppX deployment to accept it. The two-step getCert.mjs install --trusted-root flow handles all of that, but the underlying fact is worth knowing — failures present as CERT_E_UNTRUSTEDROOT (0x800B0109) from Add-AppxPackage even when SignTool succeeded.

Windows: WinAppSDK coverage

WinAppSDK has been moving some notification types onto its own APIs in Windows 11. The listener still surfaces standard toast notifications, but very new app frameworks may bypass it.

Linux: eavesdrop policy

Modern dbus-broker configurations may require a permissive policy or specific group membership for eavesdropping to succeed. The agent surfaces the error and continues idling — no graceful degradation to "your own notifications only".

Linux: GNOME / KDE coverage gap

GNOME and some KDE apps have moved certain notification types onto their own portals (e.g. GNOME's notification daemon's private interface). Those will not appear in our stream. v1 accepts the gap.

Linux: dismiss correlation

The freedesktop Notify method returns a u32 id, but eavesdropping doesn't see method replies, only calls. We can correlate NotificationClosed signals with our added events only when replaces_id > 0. Notifications without a replace id are best-effort dismissed when the platform reports closure on a matching id; otherwise the chat bubble stays until the agent is disabled.

TODOs

  • currentOnly routing. Today this falls back to broadcast — agent-server has no signal for "the conversation the user is currently focused on" (only connectionId per client). Add a "primary client" or "active conversation" concept to sharedDispatcher and have this agent build a synthetic RequestId in that mode.

  • Initial-sync option. Currently we drop notifications older than enabledAt (with a 2s grace). A future config knob initialSync: "none" | "current" could let users opt into flushing the existing action center contents on agent enable.

  • Per-app config UX. allowList / blockList are static today. A discovery flow ("you just dropped a notification from Slack — add it to the allowlist?") would be much friendlier than asking users to edit JSON.

  • Body redaction templates. redactBody: true is all-or-nothing. Per-app rules (e.g. "for Authy show title only") would be a richer privacy story.

  • Reconciliation with beginAgentThread. A parallel work-in-progress feature adds beginAgentThread() — an agent-initiated UI message API with its own bubble | toast | inline kind field. There's overlap with this agent's render-mode config. Worth a design pass once both have landed.

  • Prebuilt Windows artifacts. The C# helper exe is shipped as source — every checkout needs dotnet publish + the sparse-package register flow to make Windows work. CI should produce a signed MSIX + the published exe and bundle them into dist/. Bigger lift than just shipping a binary because of the cert + sparse-package angle, but this is the long-pole prereq for a one-click install.

  • Production cert. Today we use a dev cert from the aisystems Key Vault. Shipping outside the dev team needs an actual EV / publicly-trusted cert and a different MSIX publisher subject; the manifest's <Identity Publisher="..."> is hard-coded today.

Files

src/
  osNotificationsManifest.json     # agent metadata, defaultEnabled: false, schema ref
  osNotificationsSchema.ts         # action types: SyncOsNotifications, TestOsNotification
  osNotificationsSchema.agr        # NL grammar — "sync os notifications" / "test notification …"
  osNotificationsConfig.ts         # render config + defaults
  osNotificationsActionHandler.ts  # AppAgent: actions, commands, watcher lifecycle
  watcherProtocol.ts               # added / removed / error wire types (+ fromSync flag)
  watchers/
    index.ts                       # platform dispatcher
    windowsWatcher.ts              # spawns the helper; isWindowsHelperBuilt(); buildWindowsHelper() (full sign+register pipeline)
    linuxWatcher.ts                # in-process dbus-next eavesdrop
    noopWatcher.ts                 # macOS / unsupported platforms

The C# helper lives outside this package, alongside the other repo .NET projects: dotnet/osNotificationListener/ (Program.cs, OsNotificationListener.csproj, identity/AppxManifest.xml).

Trademarks

This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.