a sweep for remnants after 2026-08-18. nothing was broken - no dangling
import, symbol, route or permission key anywhere in src/ - but the platform
was still speaking the plugin's vocabulary in a dozen places.
the one that mattered is live code rather than prose: ChatPanelWrapper
hardcodes workspace.id === 'email' to send context: 'email', which is the
frontend half of the pair whose backend half (resolveEmailCwd) carries a
fifteen-line justification. the two ends now both say so, including that
adding a SECOND id there is the signal the platform needs a real way for a
plugin to declare a chat context.
the rest: two dead imports left by removing the settings section, an empty
'Email' type-section header, a protocol.ts section header with no members
naming the plugin's SSE design, and a queue comment pointing at
sidecar/email/sync-runner.ts - a path in another repository now.
CLAUDE.md, AGENTS.md and working-on-officer.md described email as a platform
feature, an app permission and a pm2 process. those are the files a fresh
agent reads first, so they were the highest-risk staleness in the tree.
TODO.md's whole Email section moved to the plugin's own repository. it held
OPEN items pointing at sidecar/email/store.ts, which is exactly how a leftover
list sends someone to edit files that do not exist.
the delete-account dialog promised it would remove email accounts and
playlists. both tables belong to plugins now, so that cascade only happens
where the plugin is installed - it now says 'anything an installed plugin
keeps for them' instead of naming two it cannot guarantee.
imapflow and mailparser STAY in package.json. the plugin has no package.json
of its own and resolves from the platform's node_modules, so removing them
would break the installed plugin's sidecar. the fix is giving plugins their
own dependency manifest, not deleting these.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
the sidecar, the router, the table, the screens and the account manager are
gone from src/ entirely - deleted, not commented, because a commented mount is
what assertPermissionTotality cannot see.
three reverse imports had to go first, and they are what decided the boundary.
core may not import a plugin's queries, and one of these had already proved
why: getEmailAccounts was awaited at the top level of the agent sidecar, so
when the table went away on 2026-08-13 the rejection took officer-agent into a
pm2 restart loop with every live chat session inside it (fbb6d6c7).
both survivors now read the DISK instead. findEmailAccountName() in
data-path.ts picks the newest emails.db under an account's email_accounts/
directory - no import, no query, and nothing to throw when the plugin is not
installed. the layout was already the platform's, so this costs no coupling.
the agent keeps its email_db tool and an email-context chat still opens in the
right mailbox.
the google oauth callback no longer appends /email to the dock: a plugin's
route is the plugin's business.
app-store/catalogue.ts entry deleted, and that is the whole point rather than
tidying. permissionAvailability derives from sidecar_installs and a plugin only
ever gets a plugin_installs row, so a leftover entry makes the permission
permanently unavailable - denied route, no dock tile - on a machine where the
plugin is installed and healthy. it has bitten headscale and nearly music.
two app-store tests used email as their config-only fixture; repointed at
notify, which is the same shape.
google-auth stays in core and the plugin imports it via @@/. that direction is
allowed and google-auth knows nothing about mail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
settings > integrations > personal > api keys. mirrors the dav app
password panel, which is the same problem: a secret that exists for one
response, so the new key stays on screen until dismissed rather than in
a toast.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five settings pages moved from a `*_SELECTED` global to `/settings/:page/:section`. The
sidebar entry is a react-router `<NavLink>` rather than a button holding the key in its
onClick closure, so a section is linkable, cmd-clickable and gets its active state from the
router; each page renders one `SettingsRoute` guard that canonicalises both the bare route
and a section that does not exist.
Integrations needed more than the shared factory. It builds its own sidebar, and it kept the
Enterprise/Personal tab in a second global — which is why a deep link to a Personal section
could never have worked: the link set the section, the tab stayed on Enterprise, and the
content pane said "Select a section" about a section that existed. The tab is derived from
the section key now.
Also removes the `/settings/resources` menu item (audit M8) and its two locale keys: there
has never been such a route, so it bounced to the catch-all and out to `/`.
Settings → Integrations → Calendar & Contacts sync. without this there is no way
to mint a credential from the app, and minting one was the first step of testing
the whole caldav feature on a phone.
the generated password is returned by POST and never again — it is stored as an
argon2 hash, so there is nothing to read back. that one fact drives the screen:
the new credential appears in a panel that stays until dismissed, with the
server url and username beside it, because once it is gone the only remedy is to
revoke and mint another.
the server url is read from window.location.origin rather than configured. it is
by definition the address that reached this page, so it is the one that will
work on the phone.
rows show the hint prefix and last-used date. "never used" is the tell that a
device was set up wrong, so it gets its own wording rather than a blank.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three existed to drive the platform from a chat app. The phone app does that now, so
they are dead weight — three bot gateways, three command parsers, account pairing, admin
config screens and bot tokens sitting in the database.
Gone entirely: telegram/ and whatsapp/, discord/'s bot and command handler, the shared
channel plumbing they were the only users of (pairing.ts, send-and-await.ts, types.ts,
routes.ts and its 20 config/pairing/status endpoints), the six settings components, and
their sections in the integrations screen.
What Discord keeps is the one piece worth keeping — pushing a message out — as
notify/discord.ts, configured by DISCORD_WEBHOOK_URL in the env. No UI, no pairing, no
stored credential, and it never throws: a notification that fails to send is logged and
dropped. Unset means notifications are silently skipped, which is the default state.
Kept deliberately: send-claude-code.ts and send-opencode.ts. They live under channels/ but
have nothing to do with chat apps — they are how /chat and the pipeline executor drive an
agent turn.
Also drops four dependencies with no remaining importer (discord.js,
node-telegram-bot-api, whatsapp-web.js, qrcode), and the /sync-now route added to the email
sidecar an hour ago, whose only consumer was the channel handlers.
The "Channel Models" settings section stays, with its description corrected — it is keyed
off the general access policy rather than anything channel-specific, so it governs non-owner
accounts, not chat apps. Whether that whole class still earns its place is the open
question already noted against origin-validation.
Not touched: the telegram, whatsapp and discord rows in server_integrations, which still
hold their bot tokens. Deleting rows is a different kind of decision and the SQL is in the
handover.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.
Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.
Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.
Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.
getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- apify tool: TOOL.md definition, index.ts implementation with auto-auth via OFFICER_APIFY_TOKEN, output_path for large datasets
- tools API: /tools routes (list, detail, chat, create, delete) mirroring tasks pattern
- automation UI: tools tab in sidebar, NewTool component, tool detail view
- apify integration: settings page for enterprise API key config, pi-bridge passes env var to containers
- tiktok-trends task: rewritten as agent instructions using apify tool with output_path, scripted report generation for 50KB read limit
- restrict edit/delete of native/global capabilities to Super Admin only (backend + frontend)
- tools authoring guide: TOOLS.md with full spec for TOOL.md frontmatter, index.ts execute signature, patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- relay accepts new extension connections by closing old one (last wins, code 4000)
- extension recognizes code 4000 and stops auto-reconnect (shows "replaced" badge)
- fix ping interval race where old WS close handler killed new connection's pings
- retry CDP commands on "Session with given id not found" by re-attaching debugger
- describeError() maps known errors to user-friendly badge tooltips
- persisted relay tokens restored on server start
- user-scoped token salts, token regeneration/deletion endpoints
- extension download endpoint, integrations UI for browser relay setup
- browser relay env vars passed to pi-bridge sandboxes
- browser screen layout with chat panel and prompt prefix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>