Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog — ChannelGate

ChannelGate was formerly *Claude Gateway for Slack*; entries below the rename keep their original
wording. All notable changes to the gateway, newest first. Dates are when the work landed.
This project brings Claude Code (and optionally OpenAI Codex) into Slack as a self-hosted,
per-channel-sandboxed agent. See `FEATURES.md` for the living catalog and `docs/WHY.md` for the
product overview.

> **Publication dates.** Every public release entry below carries the date the Licensor published
> it. Entries for work that never left the private repository are not publications. No version is
> relicensed automatically (license v1.2 removed the former Change Date before first publication).
>
> | License version | Effective | Published |
> | --- | --- | --- |
> | Makeitfuture Sustainable Use License 1.2 | 2026-08-25 | 2026-09-06 (with ChannelGate 0.5.0) |
> | Makeitfuture Sustainable Use License 1.1 | 2026-08-20 | never published |
> | Makeitfuture Sustainable Use License 1.0 | 2026-08-06 | never published |

## Unreleased

- **Company email auto-approval.** Admins can configure exact email domains in Users, preview
  existing Slack users and approve matching provider identities automatically. Manual revocations
  prevent automatic reapproval; no administrator privileges are granted.

- **Email accounts.** Connect an ordinary mailbox — IMAP for reading, SMTP for sending — to a
  conversation or to yourself by entering its server settings once on one form: in the admin UI
  (the conversation's Connections, or a person under Users), in Slack's ⚙️ Settings (MCP page for
  the conversation's, You page for your own), in Teams' Settings → MCPs, or through Google Chat's
  private settings form. Both logins are tested before the account is saved, passwords are
  write-only everywhere, and the mail clients run in the gateway, so a channel container never
  holds a mailbox password. The agent reads, searches, files, downloads attachments from and
  sends with the account through new `gateway` tools (`list_email_accounts`, `email_search`,
  `email_read`, `email_download_attachment`, `email_move`, `email_set_flag`, `email_send`);
  personal accounts exist only in their owner's turns. Gmail, Outlook and other Composio apps stay
  on Composio. Adds the `nodemailer` and `imapflow` dependencies.

- Price GPT-6.1 Sol at its September 29 Standard rates, including per-request long-context
  multipliers. Preserve pre-July 30 GPT-5.6 Terra/Luna rates during historical previews and
  refreshes. Defer this pricing basis's automatic historical refresh until an audited,
  explicitly authorized manual apply; forward estimates use the corrected defaults.

- Cap Registered/non-Enterprise installations at 20 registered channels and 20 registered users.
  Existing records are preserved; excess new registrations receive an explicit limit notice.
- Add Enterprise-only per-channel Approved harnesses in the admin editor and Slack Settings.
  Enforce the policy for thread pins, saved sessions, API/automation and automatic fallback;
  preserve restrictions after a license downgrade. Fix: an admin's Slack Settings on an
  Enterprise install failed to open ("An API error occurred: invalid_arguments — unsupported
  element: multiselect") because the harness picker sat in an actions block, where Slack allows
  no multi-select; it is now a section accessory, an empty approved list no longer sends empty
  initial options, and a regression test renders every Settings page against Slack's Block Kit
  rules. Slack's own validation detail now rides along in the Settings error notice.

- Scheduled Slack tasks can ask their creator questions using cards and forms. Submitted answers
  resume the correct scheduled session, including silent and retired one-time tasks.
- Add explicit admin-only sudo task schedules through MCP and the Automations editor. Each fire
  rechecks the creator's admin role before running on the gateway host with permission bypass;
  ordinary schedules continue in their channel container.

- Change Google Chat settings by typing `/settings <option> <value>` (mode, Auto, Lean, network,
  access, managers, VPN, and the thread or channel engine/model/effort). Typed changes use the same
  permission checks as the card buttons and work on a Pub/Sub-connected app, which cannot receive
  button clicks. The `/settings` card now lists the commands, and the daemon log records every card
  click it receives and every Google Chat event it ignores, by delivery path and key names only.

- Fix Google Chat card buttons (Settings Apply, tabs, Files) answering "unable to process your
  request" on a Workspace add-on Chat app: every card action now names the HTTP endpoint URL,
  which is how an add-on dispatches a click; the action's own name still travels as a parameter.

- Fix the agent directory returning an empty list on a connected Slack deployment: register
  the live Slack client for discovery and follow disconnect/reconnect. Preserve native Slack
  automation delivery, including image uploads, menus and reminder permalinks.

- Update Claude Code to 2.1.306 and offer Sonnet 5.5 in the model picker, with its
  native 1M context window and verified external-usage cache rates. Existing saved models remain valid.

- Find specialist channel agents with a live directory of purposes, examples and shared skills.
  Managers can describe/hide their channel's agent. Metadata refreshes at startup and nightly;
  recommendations respect the requester and reply audience and never transfer work automatically.

- Show connected Composio apps and accounts directly in Slack Settings → You and the channel MCP
  page, with automatic loading, refresh and account pagination.
- Show connected Composio apps and accounts on admin user pages and channel Connections pages,
  with account statuses, shared connection source, refresh controls and explicit provider limitations.

- Supply each attempt's resolved admin permission bypass decision to the assistant, so it can
  distinguish its actual launch permissions from the channel's Admin label and writable tools.
  Permission policies and container boundaries are unchanged.

- Preserve existing unavailable Cloud MCP selections when saving unrelated admin settings.
  Removing a selection refreshes the channel picker and prevents later saves from restoring it.

- Show enabled optional providers in the admin channel Runtime picker and preserve their saved
  engine/model selection when changing unrelated channel settings.

- Clear typed tokens when Slack Settings reopens a form after an asynchronous save refusal,
  including a custom MCP address rejected by DNS validation. Non-secret fields remain filled;
  leaving an existing connection's token blank still keeps its saved token.

- Keep cancelled Slack turns from restoring the loading indicator while their engine shuts down.
  Stop and cold-engine steering close progress immediately and ignore late output; cancellation
  during preflight cannot start a new indicator.

- Fix open SSH sessions retaining stale channel secrets after additions, rotations or removals.
  Refreshes now recheck the developer’s access and close sessions when access is revoked or
  their mounted workspace/runtime changes.

- Show the active model in Slack's native Stop control using a per-session display name.
  Existing apps need `chat:write.customize` and reinstallation; missing customization permission
  preserves the native Stop button with its usual name.

- Fix Slack Settings rejecting the MCP, Skills and You pages with `invalid_arguments` when
  their multi-select pickers contain options.

- Require a human approval card before managed Composio joins, invites or creates Slack channels,
  including in Auto/Admin mode and through batched actions, both identities and host runtimes.
  Refuse opaque connector code/API executors that could bypass membership approval; use
  structured actions instead. A request to share or post does not authorize adding the bot.

- **Slack Settings redesign.** ⚙️ Settings now has seven pages behind one dropdown: Agent,
  Access, MCP, Variables, Skills, Schedules and You.
  - **Edit in place:** engine/model/effort and the mode are one row of dropdowns. Tokens, URLs and
    variables open their fields on the page instead of in a second window, and the footer's Save
    stores them.
  - **Variables** have a ⋯ menu: Replace value, Allowed servers, Make readable/secret, Remove.
  - **Access** lists every member with a Use tick; unticking an approved member removes them from
    this channel only.
  - **Composio, Make toolbox, Toolbox and custom MCP servers** each have their own section, on MCP
    for the channel and on **You** for yourself.
  - **Skills and Cloud MCP** are one picker each.
  - The long explainers are gone.
- Clarify Composio file-staging identity arguments in every engine's per-run instructions:
  use `"user"` for `composio-user` and `"agent"` for `composio-agent`.
- Update the pinned Claude Code and Codex CLIs to 2.1.300 and 0.162.1.
- Update the gateway dependencies to Composio SDK 0.22.0, Express 5.3.0 and Acorn 8.19.0.
  Download filenames retain their names and required escaping; Express now omits optional quotes
  around simple attachment filenames. Regression checks cover the installed SDK's session
  creation/resume contract and public downloads with spaces, quotes and Unicode names.

- Google Chat card buttons now work: a Chat app connected through a Pub/Sub topic never receives
  button clicks (Google delivers them only to an HTTP endpoint connection), so Files / Variables /
  Settings and every other card button failed with "unable to process your request". ChannelGate
  now serves `POST /api/googlechat/events` on its HTTPS listener, verifying each request's Google
  ID token (issuer, audience = the endpoint URL, verified sender = the configured Chat app service
  account) before feeding the same transport path Pub/Sub uses, and acknowledges within Google's
  30-second window. Settings → Google Chat shows the endpoint URL and takes the app's service
  account (and, for standalone apps, the project number). The Pub/Sub subscription stays in place
  for Workspace Events reactions. Setup: `docs/PLATFORMS.md`, Google Chat step 9.
- Add Google Chat reaction controls. Chat apps receive no event for a reaction, so ChannelGate now
  maintains a Google Workspace Events subscription per space (app authentication with the
  admin-approved `chat.app.messages.readonly` scope, renewed about hourly) when Settings → Google
  Chat → **Reaction events topic** is set. The configured start emoji (🤖) runs a message as the
  reactor, 🛑 stops a running or queued turn (author or admin) and ✅ closes a tracked reminder.
  Reactions are deduplicated, freshness-bounded and withdrawn on removal; messaging is unaffected
  when reaction events are not configured or not approved.

- Google Chat `/help` looks tidier: emoji section headers, icon rows with each command in bold
  above its description, a highlighted mention hint, and no redundant "Google Chat help" bubble.
- Google Chat gains expandable help, requester-only settings/model and file controls, private
  browser credential entry, native approvals and the Files/Variables/Settings reply menu.
  Control commands remain responsive during agent work. These adapt the Teams improvements to
  Cards v2 and Pub/Sub; live Google Chat acceptance remains pending.
- Fix Google Chat audit findings: registered slash commands (`/help`, `/stop`, `/model …`) are
  read from Google's command annotation instead of the arguments-only `argumentText`, so they no
  longer arrive empty or as a bare prompt; long turns show a ticking "still connected" heartbeat in
  the working line again; a working line left by a failed edit is removed; approval refusals and
  control errors tell the clicker why, privately, without host paths or API bodies; Deny no longer
  submits the comment box; a refused browser-settings save explains the rule and keeps the
  session; a quoted top-level `/stop`/`/status`/`/clear` reaches the quoted turn in group chats and
  unthreaded spaces; a restart-interrupted button click is reported only to its clicker.

- Fix Teams 🛑 Stop and ✅ Tick reactions being ignored in group chats and channels. Microsoft Graph
  now returns those messages without reaction history, and the earlier fallback accepted only
  activation emojis. A present Stop/Tick created after its own saved cutoff and within the last
  ten minutes now acts as its reactor; removal history and deduplication still apply.

- Teams answers now end with the same reply menu as Slack: 📂 Files, 🔑 Variables, ⚙️ Settings
  and a 📄 button for each workspace file the answer named. The buttons open the existing private
  Teams windows, work on older replies and after restarts, and only for the person who asked.

- Teams file window: Word `.docx` files now show a read-only plain-text preview of the document
  body (formatting, images and layout are not shown; Download returns the original). **Edit
  file** now appears for text and Markdown files up to the browser editor's 250,000-character
  limit; it was wrongly hidden for any file over 3,000 characters.

- Keep Teams Open files/Open file buttons working when Teams accepts a channel-thread card
  without returning its activity id. The gateway used to treat that as a failed post, deleting
  the button's reference while the card stayed visible, so every click showed "This file button
  is no longer available" and the thread also showed "Teams returned no message identity".

- Preserve Teams Open files/Open file references across gateway restarts and Teams reconnects
  for their original 15-minute lifetime, with current access checks on every opening. Distinguish
  missing references, expired buttons and unknown buttons instead of attributing every lookup
  failure to a restart. Private file windows still require reopening after a restart.

- Keep Teams file callbacks responsive by removing duplicate membership requests, cancelling
  stalled read-only callbacks within 3.5 seconds, and showing a dialog error for unavailable or
  expired file controls. Settings and approval mutations retain their completion semantics.

- Add literal text previews to Teams file windows and a `share_file` agent tool that delivers
  **Open file** in the source conversation. Remove Slack-only tools from Teams and Google Chat
  sessions, preventing file sharing from calling Slack with a foreign conversation ID.

- Show a simple “Agent is working…” status in Teams and refresh the native typing indicator while
  a turn runs. Stop typing before the answer replaces the temporary status.

- Simplify Teams replies to the answer, requester tag and small statistics footer. Hide detailed
  tool/runtime progress and remove the old working message after successful failed-edit fallback.

- Keep Teams action flows in their source conversation: Files and Send file launch native windows
  from thread buttons; personal connection tokens are managed in Settings → MCPs → My connections;
  approval cards no longer send supplemental decision-link DMs. File/settings dialog contents stay
  out of shared chat, and public model cards reject forged settings actions.

- Extend the bundled `channelgate` skill with supported OpenVPN setup and task-specific VPN/SSH
  access instructions, including Secret handling, identity verification, isolation, layered
  checks and cleanup. Customers receive the reference with gateway updates.

- Allow project-local `.claude/` skills and settings in browser folder uploads, including uploads
  into existing project subfolders. The channel-root `.claude/` policy remains protected.

- Continue browser file and folder uploads after individual failures. Skipped files are listed
  with their reasons and retained for retry, while successful files are removed from the selection.
  An oversized file no longer disables uploading the rest of the batch.

- Route ongoing requests by scope before saving: memory for facts, existing skills for reusable
  procedures, existing schedules for job instructions, and channel instructions for behavior
  across tasks. The guide, managed block, tool descriptions and memory reviewer agree. Agents
  report saved versus pending changes without duplicating procedures into channel rules/memory.
- Allow schedule creators and gateway admins to read a task's full prompt and edit its prompt or
  title in place, preserving identity, timing, delivery and history. Stale content edits are
  refused instead of overwriting another edit; no delete/recreate workaround is needed.

- Allow `CLAUDE.md` in browser file and folder uploads. The new *Overwrite existing files*
  checkbox lets the uploader replace files at their original names; leaving it unchecked keeps
  numbered copies. Replacements are atomic and never write through destination symlinks.

- The `channelgate` skill now answers "what server should we host this on?": a hosting reference
  gives the recommended configuration (4 dedicated vCPU, 16 GB RAM, 150 GB NVMe, Ubuntu 24.04 LTS,
  static IPv4) and four recommended EU hosts with estimated monthly prices — Hetzner CCX23 (≈ €25),
  netcup VPS 2000 G12 (≈ €16), RoLeaf in Romania (≈ €32) and Google Cloud `n4-standard-4` (≈ €139).

- **Custom MCP servers from Slack.** ⚙️ Settings → MCPs now lists the conversation's custom MCP
  servers and your own, and lets you add, update or remove them in place. A conversation's servers
  are editable by its managers and admins; your own are yours to manage. Tokens stay write-only.
  Repeated edits reuse the Settings window; slow address/access checks acknowledge promptly,
  time out safely and recheck current authority before saving.
- Teams channel and group-chat answers now end with a native mention of the requester, matching
  Slack. Long answers tag them on the final chunk before the statistics footer; DMs skip the tag.

- Make Teams reply statistics smaller and add a gap above them, including replies that replace
  the working placeholder and background/scheduled results.

- Remove container image references from reply statistics in Slack, Teams and Google Chat.

- Fix Google Drive sync channels getting stuck after an interrupted pass. Passes now use rclone's
  self-recovery flags (rclone 1.66+), never start beside a pass container left over from before a
  restart (an orphan is removed after the maximum pass time), and a channel whose sync state rclone
  can no longer use is reported as needing a resync, with a daily admin DM and a **Resync** button
  on the channel page instead of failing silently on every sweep.

- Add Teams Graph callback diagnostics to the operational audit: receipt, validation, authenticated
  target acceptance and bounded rejection reasons, without logging callback credentials or payloads.
  Subscription retry warnings retain their numeric provider status in service logs.

- Fix Teams activation reactions being ignored when Microsoft Graph returns current reactions
  without message history. Fresh provider-timestamped additions retain reactor authorization and
  share deduplication with later history; a saved upgrade cutoff excludes older snapshots.
  Stop/Tick continue requiring explicit history, and removal history remains authoritative.
  Resolve the authorized reactor from Teams rosters using either `aadObjectId` or the legacy
  `objectId` field returned by some tenants.

- Fix Teams text model choices bypassing a dedicated channel Codex login, and resolve inherited
  model/effort defaults against the selected engine. Bound Bot Framework authentication and
  outbound requests, support placeholder cancellation, and preserve authenticated regional
  reply routes across reconnects and private file/card delivery.
- Persist ordinary Teams webhook intake before acknowledgement, recover queued work without
  replaying uncertain running turns, and keep Stop/status reachable behind occupied dispatch
  slots. Cancel durable queued turns under the existing author checks and protect text/native
  model selection against queued work and concurrent runtime changes.

- Make Teams `/help` easier to scan with a quick start, highlighted common commands and six
  expandable topics. Shorter descriptions and readable reaction names replace the long initial
  guide; the full text remains available when native card delivery fails.

- Show all six section buttons in one centered row in the Teams settings popup and open
  `/settings` on General with its form already displayed.

- Teams `/settings` now posts a compact **Open settings** button that opens a private popup
  inside Teams. All six settings pages, saves and confirmations stay in the popup instead of
  expanding the chat card. `/secrets` opens its Variables page. Every opening preserves the
  original conversation/session, rechecks current access and has separate expiring form state.

- Slack's native **Stop** button: the app manifest now subscribes to `agent_session_stopped` (and
  `agent_session_title_changed`) and every turn drives the thread's Agent Session natively
  (`agents.sessions.setStatus` / `rename`, legacy `assistant.threads.*` as the bridge fallback), so
  Slack renders its own Stop control on a running thread; pressing it cancels that thread like a
  `stop` message. Paste the updated `slack-app-manifest.json` and reinstall the app to enable it.
  The manifest also registers the `/model` slash command the gateway already handled.
- Richer native data tables: `slack_post_table` accepts link, formatted, mention and row-button
  cells (lightweight Markdown in strings or explicit cell objects) and Slack's current limits of
  200 rows / 20,000 characters.
- Composed replies: the new `slack_compose_reply` control tool attaches charts, tables, cards,
  link rows and collapsible details panels to the final streamed answer, so one message carries
  the takeaway and its visuals; a rejected block set is retried without it so the answer and its
  footer always land.
- Native 👍/👎 answer feedback on every reply footer, recorded as `reply_feedback` events; a 👎
  privately invites the correction in the thread.

- Fix completed Codex turns being reported as failures after recovered reconnect notices; retain terminal failure and unsafe-replay safeguards.
- Restore bounded Claude watchdog continuation for cold and warm silence-budget failures using typed error details; retain Stop and ambiguous-kill exclusions.
- Add observed pending-tool context to Claude and Codex hard-kill diagnostics without attributing the cause, suppressing diagnosis or replaying ambiguous work.

- Refuse runtime selections for enabled optional providers whose own credential or endpoint is
  missing, preserving existing pins and reporting the Settings repair. A complete provider
  configuration and its default selection can be saved together without account fallback.
- Processed Slack originals stay removed on ordinary thread replies after a successful download.
  Explicit attachment downloads remain available, and failed downloads can still retry.
- The bundled gateway guide now explains Composio discovery, independent tool batches and Python
  workbench scripts for bulk operations, including account selection, remote result files and safe
  file staging. It applies on every chat surface without overwriting operator guide overrides.
- Claude Cloud MCP discovery now marks connected servers without an admissible transport as
  unavailable, suppresses unsafe CLI targets, and rejects new selections against fresh host
  definitions. Existing invalid picks remain removable and are still dropped safely at run time.
  Teams Settings applies the same fresh admission check after cached discovery and hides Activate
  for unavailable definitions while retaining Deactivate for existing grants.
- An unlinked channel's Drive status reports gateway readiness without claiming that the channel
  will sync on the next sweep. It explicitly asks for a folder link before channel sync can start.
- Public automation documentation now covers run history, task outcomes, quiet execution and
  existing-thread delivery, with the exact `get_schedule_runs` and `update_schedule` controls.
  The documentation build again accounts for every registered control.

- Far fewer approval cards. A gateway tool that changes channel or gateway state is now automatic
  for the admin or channel manager who asked for it (modes, network, folders, MCP servers,
  secrets, tokens, skill settings, gateway update and restart, the license key, instruction
  replacement), and refused outright for anyone else. Only eight cards remain, for changes that
  would silently reach other channels or leak a secret: Admin mode, the gateway guide, admitting a
  skill source, granting skills organization-wide, adding to a template, approving a skill
  proposal, approving a server for a hidden secret, and making a secret readable. Every remaining
  card is durable: it never expires, survives restarts, and the click applies the exact saved call.
  A secret's card is approved by its tier (admin for the organization's, anyone working in the
  conversation for its own, the owner for a personal one). "Approve forever" is a channel manager's
  call, and new channels let approved members manage their own settings by default.

- Slack and Teams help now cover current settings, variable scopes and secret approvals, custom
  MCP connections, skill authoring, automation controls, dedicated Codex sign-in and self-service
  SSH. Slack help uses the current mode names and explains file sharing and finite loops.

- **Automation health and quiet delivery.** Run history now distinguishes an agent finishing from
  its task succeeding, retains connection checks and safe logs, and shows per-automation health,
  timing, tokens, cost and delivery failures. Tasks can run silently and send only useful results,
  keep failure alerts independently configurable, and deliver repeated fresh runs into a chosen
  thread or a daily thread. Healthy empty checks remain recorded without routine messages. Missing
  outcome reports remain unreported; deleted/one-time tasks retain their history.

- **Custom MCP servers.** An admin can add a remote MCP server by URL and Bearer token to a
  conversation (Connections → *Custom MCP servers*, reached as `custom-<name>`) or to one person
  (*Personal MCP servers*, reached as `my-<name>` in that person's runs only). Claude and Codex
  both receive them. The gateway connects on the agent's behalf, so the token never enters the
  container. URLs must be HTTPS and public, checked at save and on every connection. Tokens are
  write-only. Servers that need OAuth still go through Composio.
- Admin settings now offer independent message-activation emoji lists for Slack, Teams and Google
  Chat. Slack keeps its existing selection; Teams applies its list to native and Graph reactions
  and shows it in help. Teams configuration changes exclude older queued/history activations.
  Google Chat's selection is saved, with an explicit note that its current transport does not
  receive reaction events.
- Settings can connect a Perplexity subscription for research delegation from Claude and Codex.
  Agents receive quick/deep research, model discovery and quota tools with citation results;
  the real login stays in the gateway and containers receive a protected relay placeholder.
  The optional unofficial client requires network access and is omitted in Lean mode.

- Teams `/model` now works like Settings → General: choose the engine and press **Load models**,
  and the Model and Effort lists show only that engine's choices; Apply saves. Previously one card
  listed every engine's models together. Apply after changing Engine without Load models loads
  that engine's models instead of saving.

- Teams settings now show only models for the selected engine in each channel/session list.
  After changing Engine, Load models refreshes the choices without saving; Apply saves the
  complete selection for that scope.

- Teams `/model` now opens an engine, model and effort picker with an Apply button for the
  current session, alongside its current selection. Typed changes remain available.

- Teams Graph reaction intake now recognizes Heart eyes robot when Graph encodes it as 😍
  with the exact provider name and no custom artwork. Ordinary Heart eyes and missing metadata
  remain inactive; a saved cutoff prevents older reactions replaying after the upgrade.

- Teams can activate a request with 👽 Alien (`:alien:`), its bare ID, Unicode form, or documented
  `1f47d_extraterrestrialalien` ID. Help recommends Alien; robot and Like remain supported.
  A separate saved cutoff prevents older Alien reactions replaying after the upgrade.

- Missing personal skill grants or dependencies no longer prevent unrelated requests from
  running. Available skills load normally, and streamed/final replies warn about omissions,
  including after engine fallback. Ownership and confinement checks remain enforced.

- Teams reaction activation now accepts the exact Heart eyes robot picker shortcut
  `:hearteyesrobot:` as well as its bare event ID. 👍 Like is an alternative activation reaction. Native `like` and Graph Unicode
  thumbs-up values, including skin-tone variants, use the existing authorized reaction path.
  Teams help shows the Heart eyes robot shortcut and Like alternative; legacy robot IDs remain
  recognized. A persisted cutoff prevents newly recognized aliases from replaying older reactions.

- Teams help now shows Teams reaction IDs and picker names instead of Slack shortcodes.
  Heart eyes robot no longer displays the mismatched plain robot glyph, and the guide explains
  that reaction codes must be selected through the picker rather than sent as text.

- Teams Stop sign reactions now cancel the selected session’s active and queued requests with
  author/admin checks. Tick button reactions acknowledge tracked reminders, including escalation
  notices. Native and Graph intake recognize their Teams IDs; help describes the supported actions.

- Teams `/help` now uses a native card with separate headings, spaced paragraphs, emoji rows
  and commands, avoiding the run-on layout caused by Markdown soft line breaks. Card delivery
  falls back to the complete spaced guide, and quoted group help cards keep their session.

- Teams help identifies Heart eyes robot, Stop sign and Tick button by their emoji names.
  Robot activation now recognizes the documented Teams `hearteyesrobot` and `smilerobot` IDs.

- Teams `/help` now includes the practical usage guide for files, settings, skills, memory,
  schedules and background work, with Teams-specific session controls and command examples.

- Teams and Google Chat now show the model and current app/tool call while a turn runs, and the
  same model, duration, token, optional cost, context and image footer as Slack on its final reply.
  Live status updates coalesce within the surface edit budget and never show tool payloads.

- Container turns now prepare a protected Claude login even when Codex, Qwen or Qwen EU is the
  selected engine. Explicit nested `claude -p` commands authenticate through the host's existing
  relay; Qwen's main process retains its own provider settings. No additional interactive login
  or thread switch is needed when the host already has Claude authentication.

- Teams `/settings` now offers the same six sections as Slack, including channel defaults and
  session overrides, variables, MCP connections, skills, automations and session resume. The
  console opens in the original channel/thread or chat, uses explicit Apply buttons and confirms removals without revealing
  stored credentials. `/secrets` opens Variables directly.
  Buttons update the same requester-bound card. Shared cards omit personal/organization
  credential metadata, ungranted private connection catalogs and administrator session commands.
  `/settings` initially shows only the section menu; selecting an option opens its settings below.
  General runtime settings now use one Apply to channel or Apply to thread button to save engine,
  model and effort together. Stale and incompatible forms cannot partially change the runtime.

- Users already allowed in a channel, including named guests, can enable their own SSH access
  without manager or admin approval. Registering/removing personal keys and revoking one’s
  own SSH grant are also self-service. Changes for other people remain manager controlled.

- Background shell jobs (`run_in_background`) inside a channel's container now start directly in
  Auto mode, with no "Run it" card: the container is the boundary and Auto mode already
  auto-approves every foreground command there, so the former admin-only click only stalled
  non-admin authors. A job on the daemon account itself (the `/sudo` host lane) still needs an
  admin's exact-command approval. Every agent-type approval card now says on the card who can
  approve it ("Only a gateway *admin* can approve this; anyone eligible may Deny or Comment").

- Gateway Settings now lets admins map short names to an engine and model. In Slack, select the bot mention and type `:astra` after Slack's inserted space (`@agent :astra`) to pin that model to a thread, with an optional task after the name. Mappings can be updated as models change.

- Admin channels now keep non-admin members and guests in a separate project container with
  Worker permissions and the selected Auto/Lean options. Only current organization admins
  receive the optional host-home mount. HOME, scratch files, artifacts and engine histories
  are separated, and switching author lanes starts fresh history.

- Conversations can filter by Google Chat, Slack or Teams alongside search and Channels / DMs.
  Teams channel names now use `#team-channel`, resolving missing names through the bot directory
  on incoming messages while preserving conversation IDs, work folders and settings.

- Update runs the host Bash entry point, repairs exact dependencies and provisioning even when code
  is current, checks image executables without provider sign-in, and uses a bounded graceful restart
  with a longer startup readiness window. Provider authentication no longer blocks gateway repairs.

- Codex now continues a partially completed turn in the same session after a model-capacity
  refusal. It avoids rerunning completed tool actions and tries another model unless the thread's
  model was explicitly pinned.
- Update smoke now binds and marks its disposable channel live for the duration of the probe,
  allowing the Codex login relay through the egress proxy and releasing it on cleanup.
- Scheduled checks can run every fixed number of days and DM their creator only when the result
  begins with a configured match prefix. Routine checks make no Slack posts.

- Add `@agent /fork <new message>` for Claude and Codex sessions in Slack. It starts a separate
  engine branch in a linked new thread and runs the supplied message there.
- Allow `@agent /fork :model-shortcut <new message>` to select the fork's model on the source
  engine, preserving the original thread's settings. Invalid or unauthorized shortcuts are
  rejected before creating a thread.
- A `/fork` model shortcut for another engine now creates a linked fresh session and carries
  the source Slack conversation into it, like switching an existing thread's engine. Same-engine
  shortcuts retain native session forking; clean threads continue without history replay.
- Fix a history-transfer deadlock when `/sudo` crosses a container whose mounts need recreation.
  Transfer preparation now excludes its own lease, reports waits, handles Stop, and times out
  into transcript recovery instead of hanging indefinitely.
- Google Chat now reports a fatal Pub/Sub receiver error as a failed connection, stops intake,
  and gives an actionable Subscriber-role hint for permission errors. Reconnect restores status
  without allowing an old connection's late error to overwrite it.

## 0.6.3 — 2026-10-11

- Refresh the locked MCP SDK, URI and IP parsing dependencies to patched compatible releases.

- Add optional `conversationKey` to the HTTP run API: keep related external events in one
  persistent Slack thread, preserve agent context through the existing run queue, and expose
  thread timestamps, reuse flags and permalinks. Replace deleted roots safely and report Slack
  access failures without silently starting a separate headless conversation.

## 0.6.1 — 2026-10-06

- Channel Runtime now shows only engine and model controls for the shared login. Dedicated Codex
  sign-in uses a method dropdown: choosing ChatGPT starts the device flow and clicking its code
  copies it; the API key field appears only for that method. The shared sign-in moved to Settings.
  Slack Settings names the dedicated login and its channel defaults. Codex Cloud MCP discovery
  and launch now use that channel account when selected.
- ChatGPT sign-in displays the device link and complete code from current Codex CLI output;
  proxied hosts pass the public TLS CA bundle to the login process.
- A channel using its own Codex login now stays on Codex across settings, `/model`, and existing
  threads. The settings page hides the redundant engine picker and
  explains login storage without showing the internal path.
- **The agent can send any file into the thread.** New gateway tool `slack_share_file` posts a file
  from the channel's working folder — PDF, Word, Excel, PowerPoint, ZIP, images, HTML — into the
  current thread as the bot, up to 25 MB, exactly like the file explorer's Share button. "Trimite
  fișierul" no longer ends in a "which Slack account?" question card: a Composio upload is only for
  other channels or DMs now. Files outside the channel folder (including the mounted operator home)
  and symlinks out of it are refused.

- Completed nested background reports stranded by the earlier `invalid_thread_ts` error get one
  bounded recovery attempt from their saved output after restart. Their work is not rerun.
- Channel Runtime settings now ask for the Codex login source first. Shared gateway Codex sign-in
  is available from the admin Settings page.
- **Nested background agent reports reach their Slack thread.** Delivery now turns an agent's
  synthetic session key into the launching thread timestamp before posting the report and menu.
  Scheduled reports with synthetic keys post at channel level. This prevents `invalid_thread_ts`
  from stranding a completed report.
- Admin channel settings now start ChatGPT device code sign-in or accept an OpenAI API key for a
  dedicated Codex login, show its status, and select it after success.
- Channels can select a separate host-side Codex login for ChatGPT subscription access or an
  OpenAI API key. Proxy-mode containers receive only channel-bound credential placeholders.

- **Automatic engine switches now stay with the conversation.** When Claude reaches its limit and
  Codex answers, the next message resumes that Codex session instead of retrying Claude. The same
  holds when Codex switches to Claude. Existing threads with a successful fallback session adopt
  it on their next message; a manual thread engine or model choice still takes precedence.

- Overview now draws separate stacked columns for each time bucket, shows model totals when a
  source, channel or user bar is hovered or focused, and arranges usage sources beside the three
  time charts above Models, Channels, Users and Skills.

- **The folder-generator test no longer scatters projects across the operator's home.** Its custom-workdir fixtures now live under one `ChannelGate Testing` folder, are removed when the test process exits, and stale runs are swept before the next test suite.
- **A provider usage limit no longer blocks an update.** Before and after updating, the updater sends each engine a tiny test prompt inside a throwaway container. A reply like "You've hit your weekly limit" used to refuse the update, even though it proves the engine starts, logs in and reaches its provider. It now counts as reachable. A broken login or engine still stops the update.

## 0.6.0 — 2026-09-28

- **Claude model choices now show exact versions.** The `/model` picker and admin selectors list
  Opus 5.5, Fable 5.1, Sonnet 5, and Haiku 4.5. Duplicate moving aliases and special modes no
  longer crowd the normal list. Existing alias settings keep working and stay visible when selected.
- **Codex no longer sends usage metrics to OpenAI.** Claude Code's telemetry was already off, but Codex still sent OpenAI its anonymous usage and health metrics by default. Every Codex process the gateway starts now runs with `analytics.enabled=false`: chat turns, background work, the memory reviewer, SSH and VS Code sessions, and the gateway's own checks.
- **A channel's database can be read page by page, and counted.** `query_channel_database` used to return at most 100 rows with no way to ask for the next ones. Ordered by a table's primary key, a read now also returns `nextCursor`; passing it back as `after` gets the next page, until it comes back empty. The new `count_rows` returns how many rows match, so an agent can show that nothing was missed. The extractor image has to be rebuilt (`npm run vpn -- build --channel ID`, then VPN off and on); until then the running one keeps working and a paged or counted request says it needs the rebuild.
- **A connector outage at the provider no longer looks like a gateway bug.** When Composio or a toolbox answered a connection attempt with an error, a container run just lost the connector and the daemon logged only `remote MCP unavailable`. A brief 502/503/504 or network blip is now retried once after a second, so it no longer drops the connector for the whole turn. A lasting failure is logged with the server and the upstream status, for example `remote MCP unavailable (composio-user: upstream HTTP 502)`. The container still sees only the fixed sentence.
- **Slack Settings has an Automations page, and the tabs are reordered.** The tabs now read General
  · Variables · MCPs · Skills · Automations · Resume. **Automations** lists the conversation's
  scheduled tasks, reminders and `/loop` wake-ups with their next run time, and lets anyone allowed
  to use the agent there pause or resume a recurring schedule and delete any of them. Creating one
  is still done by asking the agent.
- **Every AI reply ends with the same menu: 📂 Files · 🔑 Variables · ⚙️ Settings.** Replies used to show the menu inconsistently. Errors had none, "🛑 Stopped." still showed the removed 💻 Resume button, and API-run, scheduled, background and restart-recovery answers had no menu or lacked Settings. Replies also showed bare icons, while `/menu` spelled out the labels and still offered Resume. Now every answer, error and stop in Slack ends with all three labelled buttons, after a model switch, an API request or anything else. A long answer carries the menu on its last message instead of the first. `/menu` shows the same three buttons. The resume command stays in Settings → Resume Session and `/resume`. A menu under an automated post (no Slack requester) opens for whoever clicks it, with that person's own permissions.
- **Allowed domains is back as an option** on the add-variable form and the admin page. Filling it in restricts a secret to exactly those domains: no approval cards, and the value is never sent anywhere else. It also now works for tokens sent in headers other than Authorization, such as `x-api-key`. Leave it empty to approve servers on first use instead.
- **The admin page lists variables in a table:** name, masked value, type (🔒 Secret or 👁 Readable, with the reason on hover), the servers a secret may be sent to, who set it (shown as a person's name, not a Slack id) and when.
- **Adding a variable asks one question: is it a secret?** The Visibility choice and the domains field are gone from the add-variable form and the admin page. One **Secret** checkbox remains, ticked by default. The gateway handles the rest: where the variable may be sent is learned through admin approvals, and an email or database password stays usable even when marked secret. Untick it for plain configuration such as an id, a region or a URL.
- **"Secrets" are now "Variables", and you choose how each one is shared.** The Slack menu, the message footer, the Settings tab and the admin page now say *Variables*. Adding one offers **Visibility** (Auto, Hidden or Readable) and an optional **Used on domains**. A variable given domains stays hidden and works only on those domains, with no approval card. A variable that looks like an SMTP, database or signing login stays readable even when Hidden is chosen, because it cannot work as a placeholder, and the list says so. Leaving the fields empty on an update keeps the current settings.
- **Every secret is hidden by default, and an admin approves where it may go.** A secret the gateway had no rule for used to reach containers as its raw value. Now, unless it looks like a password, connection string, signing key or configuration value (those stay readable), containers get a placeholder. The first time a program sends it to a new server, the proxy refuses that one request and posts an approval card in the thread: *Use secret VERCEL_PAY_MAKEITFUTURE on api.vercel.com?* After an admin approves, that server keeps working for every run, schedule and SSH session. A secret is never sent to the model APIs. New tools: `set_secret_mode` (hidden / readable / auto) and `allow_secret_host` (admin; approve a server without waiting for a card). Existing secrets without a rule switch to this on the next run, so each asks once per server.
- **Templates are the team skill sets, and admins curate them from chat.** An admin can now add skills to a template (Development, Sales, …) or remove them straight from chat with `update_skill_template`. Anyone else can ask for a skill to be added to a template, and an admin approves the request. There is no separate "team" scope for skills.
- **Skills no longer ask for approval you already gave.** Creating, updating or deactivating a personal or channel skill used to post an Approve card every time, admins included, and the agent often asked where a new skill should go. Now a skill the user asks for is created as a **channel skill** of the conversation (a personal skill in a DM) with no card and no question. Any member of the channel can edit or deactivate the channel's skills. The agent mentions every change in its reply. Admins moderate only the **organization** library: an admin's organization skill is created at once, while anyone else's request becomes a channel skill plus a proposal for an admin. A member's edit to an organization skill becomes a change proposal, and deleting a skill from the whole catalog is an admin's call. Skills synced from the gateway's own publish repository can now be edited and are pushed back; skills from sources the gateway cannot write to are extended with a companion skill instead. `set_skill_scope` is now admin-only.
- **Codex's read-only sandbox also works in whole-home Admin channels.** Codex keeps its app-server socket in a fixed `/tmp/codex-daemon-<uid>` and requires that folder to be its own mount, owned by the user with mode 0700. In a channel that mounts the operator's home, even the `/tmp` volume lives under that home, so member turns there (held read-only by the home guard) still could not run commands. Every container now gets a 1 MB tmpfs on exactly that folder. Existing containers are recreated once.
- **Codex Read-mode commands now actually start.** Each channel's `/tmp` and `/var/tmp` were folders inside the channel's artifact folder, which is also mounted at its own path, so the same files were visible twice inside the container. Codex's sandbox refuses that ("app-server socket directory has an unsupported host mount … remove the bind-mount alias"), so Read-mode Codex still ran no command after the capability fix. `/tmp` and `/var/tmp` are now per-channel podman volumes (`<container>-tmp`, `<container>-vtmp`), kept across stops and recreates like the home volume. Existing containers are recreated once; files an agent left in the old `/tmp` stay on the host under `~/ChannelGate/.runtime/<platform>/<slug>/tmp` but no longer appear in `/tmp`.
- **Members can no longer write to the operator's home through a whole-home Admin channel.** With *Admin channels can access the host home* on, the home is mounted read-write for the whole channel container, and a non-admin author's turn in that Admin channel still received the shell and file-writing tools, so it could change any file in the home without a prompt (CTR-30, 2026-09-27). Every run there that is not an admin author's is now read-only: Claude's shell and write tools are denied outright, Codex runs in its read-only sandbox, and plugin command servers are refused. Admin authors are unaffected.
- **Codex Read-mode channels can run commands again.** Codex 0.156.1 enforces its read-only sandbox with bubblewrap only, and bubblewrap will not start while the process holds the container's added capabilities ("bwrap: Unexpected capabilities but not setuid"), so every command failed even with bubblewrap installed. A sandboxed Codex run in a container now starts without those capabilities: reads work, writes are refused. The ignored `features.use_legacy_landlock` switch is gone (EN-01 on Codex, 2026-09-27).
- The egress proxy now presents its CA after the per-host leaf, so the in-container browser's CA pin matches and agent-browser loads HTTPS pages without `--ignore-https-errors` (EGR-03, 2026-09-27). Codex failure messages no longer lead with the CLI's "Reading additional input from stdin..." notice (EN-09).

- Channel image spec 1.6.1: the image now ships `bubblewrap`. Codex 0.156.1 refuses to run any command in a read-only sandbox without it ("filesystem-restricted execution requires bubblewrap"), so Read-mode Codex channels could not execute a probe since the 0.156.1 pin (found by the 2026-09-27 live campaign, EN-01/CTR-06/MD-02 on Codex). Rebuild with `npm run build:image`.
- **The browser works behind the egress proxy.** Chromium's proxy and certificate flags were joined
  with a space, which agent-browser does not split, so every page failed with
  `ERR_INTERNET_DISCONNECTED` in a proxy-mode channel. They are now comma-separated.
- **Leftover credential files are cleaned out of channel containers.** A crashed or interrupted run
  could leave its MCP config or Codex secret bundle in the channel's artifact folder, where later
  turns could read it (files from before the MCP relay held real Composio and toolbox tokens). The
  gateway now deletes such files older than six hours before a channel container is created or
  started, and at boot for containers already running.
- **Idle channels no longer flood the audit log.** An idle warm Claude process's telemetry, refused
  because nothing is running in the channel, is counted in the egress status instead of written as
  an audit row for every attempt. Every other refusal is still logged.
- **Codex status and error text no longer shows Node's proxy warning.** The
  `[UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental` line is silenced in proxy-mode Codex
  runs and filtered out of the status row and failure messages.
- **Claude sends no telemetry from proxy-mode channels.** Claude runs behind the egress proxy now
  set `DISABLE_TELEMETRY=1`, so an idle warm engine stops sending telemetry the proxy would refuse.
  Feature flags, the version check and claude.ai plugin downloads are unaffected. A channel secret
  named `DISABLE_TELEMETRY` is now refused as reserved.
- **Organization secrets are named as organization secrets.** When a run's only secrets were
  organization-wide, the credential list never said so, and an engine could call them channel
  credentials. The scope is now stated whenever any secret is organization-wide or personal.
- **Codex's sign-in no longer sits in every channel container.** Until now each channel container
  had the gateway host's real Codex login file, refresh token included, mounted read-write, so one
  channel could read it and every channel shared it. Behind the egress proxy a container now gets
  its own access-only sign-in: a stand-in token the proxy swaps for the real one on OpenAI's and
  ChatGPT's servers, and no refresh token at all. The gateway keeps the real login fresh with a
  cheap Codex turn on the host when it is two days from expiring. Keep the host signed in with
  `codex login` as before. Each channel container is recreated once on its next turn. The legacy
  open-network mode and an API-key Codex login still mount the real file. Rebuild the image
  (`npm run build:image`, still spec 1.6.0): `cg-init` now also removes an old copied Codex login
  from a channel's home.
- **Secrets without an egress rule are withheld by default on new installs.** On a fresh install
  *Withhold unprotected secrets* starts on: a secret with no built-in rule and no *Used on hosts* is
  not given to channel containers. Existing installs keep their current behavior; turn it on in
  Settings → Container runtime. `list_secrets` now ends with a **Finding** that names every secret
  without a rule and says how to protect it.
- **A check keeps secrets out of the files channel containers can read.** `npm run check:static`
  now fails when code writes a token, a relay's token or a resolved run environment into a
  channel's artifact folder. The unused `remote-secret-bridge` helper is removed.

- **SSH and VS Code sessions hold placeholders, not secrets.** A developer's SSH session into a
  channel container now gets the same `cgph_…` placeholders a turn gets, and Claude's login in the
  session (and the editor's token file) is the channel's login placeholder. Nothing a session
  writes holds a real protected value. Tools such as `gh` and `vercel` work as before through the
  gateway's egress proxy. The proxy settings reach every SSH shell, VS Code terminal and your own
  `agent-browser`. The editor token file is now also removed when the channel's last session ends.
- **Your personal secrets pause while someone else is attached.** While another person has an SSH
  session open in a channel, your personal secrets stop working there. Everyone else's stop working
  while you are attached. The agent's credential note says they are paused rather than failing
  with an unexplained 403.
- **Outbound SSH from a session goes through the egress proxy.** The image ships
  `/opt/channelgate/bin/cg-egress-connect`, an SSH `ProxyCommand` through the egress proxy. A
  session's `GIT_SSH_COMMAND` already uses it (github.com only, with *Allow network* on). For your
  own `ssh`, add `-o ProxyCommand='/opt/channelgate/bin/cg-egress-connect %h %p'`. The proxy
  cannot supply an SSH key. `ssh -L` forwards to hosts outside the container no longer work;
  forwards to the container's own ports and `-R` are unchanged. Rebuild the image
  (`npm run build:image`, still spec 1.6.0) to get the helper.

- **Channel containers now reach the internet only through the gateway's egress proxy.** Every
  channel container runs with no network of its own. A small forwarder inside it hands each
  connection to the gateway, which enforces the channel's *Allow network* switch on every request
  (off: only the AI engines and the channel's selected connectors; on: public hosts, never private,
  loopback or cloud-metadata addresses) and logs every refused or blocked destination. The switch
  is no longer advisory, and `/mode`, `/status` and the run record say so. A flip applies to the
  next request without recreating anything.
- **Secrets become placeholders the container cannot use elsewhere.** A GitHub, Vercel, Supabase,
  Make or Composio token, and any secret an admin marks with **Used on hosts**, reaches the
  container as a `cgph_…` placeholder. The gateway swaps in the real value only on that secret's
  hosts and only while the channel has work running; a personal secret only while its owner is the
  one working, and never while another person's turn, background job or SSH session is active
  there. The relayed Claude
  login is a placeholder too. Rotation takes effect on the next request; removing a secret kills
  its placeholder. Secrets without a rule are still injected as before and are marked
  **unprotected** in `list_secrets`, the admin UI and the agent's own credential note. The new
  *Withhold unprotected secrets* setting keeps them out of containers entirely.
- **New container settings.** Settings → Container runtime gains *Legacy open network (no egress
  proxy)* — off by default; on restores the old open bridge network and raw secrets — and
  *Withhold unprotected secrets*. The admin API accepts `rawNetwork` (a channel that needs raw
  sockets beside the proxy) and `egressRawHosts` (hosts whose SSH/Postgres ports are tunnelled).
  If the proxy cannot start, container runs stop with the reason instead of running unprotected.
  After a restart, containers that kept running get their network back immediately. Avoid
  multi-tenant host suffixes such as `*.vercel.app` in *Used on hosts*. A Qwen provider key and a
  daemon `CODEX_API_KEY` are still given to containers as real values, and a self-hosted Qwen
  endpoint on a private address cannot be reached in proxy mode.
  TLS clients in the container trust the gateway's own CA through the usual CA variables; a tool
  that reads none of them needs `/run/channelgate/egress-ca.pem`. After updating, run
  `npm run build:image` (image spec 1.6.0 now also ships the forwarder; the updater does this for
  you).
- **Fix: Codex in Composio SDK mode works in containers.** A Codex run in a channel container with
  Enterprise SDK-mode Composio started its Composio connection without the run's grant, so the
  gateway refused it and Codex had no Composio tools. It now connects like the other relayed
  servers.
- **Composio and toolbox tokens no longer enter a channel container.** In a container, the
  `composio-user`, `composio-agent`, MakeItFuture toolbox and Make toolbox connections are now
  reached through the gateway itself: the engine holds only its signed run grant, and the daemon
  dials the service with the real token and relays the tools. Before, the token sat in a file in
  the channel's run folder that any process in the container could read. This applies to Claude
  and Codex turns and to SSH sessions. Direct-host `/sudo` threads are unchanged. A rotated token
  still restarts the warm Claude process. After updating, run `npm run build:image` (image spec
  1.6.0; the updater does this for you).
- **Agents can now explain how to set up a channel's VPN.** The chat operating manual has a
  channel VPN page. It covers who may turn the VPN on or off, read its status or query the
  database. It also has the host-operator runbook for another channel, with the exact
  `npm run vpn` steps, the profile and Secrets requirements, what each failure means and who fixes
  it, and how to rotate or retire the service. It says plainly that provisioning cannot run inside
  a channel container. It never tells anyone to paste a profile or password into chat.
- Dependency refresh: the Composio SDK moves to 0.19.0 (the session API the Enterprise SDK mode
  uses is unchanged), `mcp-remote` to 0.14.3 in both the daemon and the channel image, and `zod`
  to 4.6.5. `hono`, pulled in by the MCP SDK, moves to 4.13.9 for its moderate advisories. After
  updating, run `npm run build:image` (the updater does this for you) so channel containers get the
  new `mcp-remote`.

## 0.5.7 — 2026-09-25

- **An HTTP API run now works like an admin's message in its channel.** The run API key is an
  admin credential. An API run gets the channel's mode as an admin would, including Auto and, in an
  Admin channel, the permission bypass. It also gets memory (search, read and save, plus the
  post-reply memory review), skills, connectors, the admin gateway tools, and the same tool
  approvals. It acts as one fixed `api` principal, never as the `author` it names. So it uses the
  channel's shared agent Composio account and gets nobody's personal tokens, secrets, skills or SSH
  keys. Background work and schedules it starts are owned by `api` too. Per-user API keys are
  planned.
- An API run joins its thread's queue. A Slack reply in a channel-backed API thread gets the usual
  Steer / Queue / Cancel choice instead of running at the same time, and a Slack stop or steer in
  that thread stops the API run.
- A per-run API `mode` no longer rebuilds the channel's container. On an Admin channel with the
  host-home switch on, a `read`/`worker`/`auto`/`lean` run used to drop the home mount, wait for
  every run inside, and recreate the container (killing detached jobs). The next ordinary turn then
  recreated it back. The mode now narrows tools only.
- The `resumeCommand` an API run returns now opens the session inside the channel's container, like
  the Slack Resume control. The old host command answered "No conversation found".

## 0.5.6 — 2026-09-25

- Codex over SSH now gets what a chat turn's Codex gets: the gateway tools, your own and the
  channel's Composio accounts, the channel's MCP servers and your channel secrets, starting in
  the channel folder. `with-secrets <command>` runs any other command with those secrets.
- Slack **⚙️ Settings** pages are tabs again: one row — General · Resume · MCP · Skills · Secrets —
  with the open page highlighted, instead of the *Page* dropdown.
- Google Drive sync now syncs the channel's whole folder instead of a `Drive/` subfolder. Agent
  instructions and skills, channel memory, secrets, `.git` and dependency trees never sync, a
  `.driveignore` file adds a channel's own exclusions, and symlinks are never followed. rclone now
  runs in a throwaway container that sees only the channel folder, so a planted link can never
  write onto the host (this also closes that hole in the old subfolder sync). A folder that
  contains the home or the gateway's data is refused.
- VS Code over SSH now starts in the channel's folder: File → Open Folder in a Remote-SSH window
  opens at the channel folder (or its custom folder) instead of the container's home.
- Handing a generated file to Drive, Gmail or another Composio tool now goes through the gateway's
  staging on every engine: the rule rides each run's Composio identity notice, so Codex no longer
  falls back to pushing the file through Composio's workbench as base64.
- Google Drive sync no longer stalls on a folder that was empty when it was linked. The first sync
  of two empty sides succeeded, but every later sync then failed (rclone: "Empty prior Path1
  listing"), so files never moved. A sync record that holds no files now triggers a fresh resync.
- Slack **⚙️ Settings** no longer claims a channel pinned to Claude inherits the Codex default
  model (or the reverse): an unset model or effort now names what the channel's own harness would
  actually use. The web admin's VPN status also stops saying "not configured" twice.

- Sending a generated file to Drive, Gmail and other Composio tools works again — or rather, works
  for the first time on a gateway using personal Composio tokens. `stage_file_for_composio` sent the
  stored token to Composio's REST upload, which only accepts project API keys; the tokens stored
  here are consumer (MCP) keys, so every stage failed with "Invalid API key" and the model told the
  user their key was broken. With a consumer key the gateway now stages through the hosted MCP's own
  workbench, daemon-side and in chunks, so the file still never passes through the conversation
  (limit 25 MB there, 100 MB with a project key). A real problem now reads `Staging failed:`;
  `Staging refused:` is kept for files that are not the conversation's own (outside its folder, a
  symlink, not a regular file).
- Security: staging a file for Composio now reads the exact file whose location it checked. It
  used to check the file, close it and open it again by name, and the conversation's container can
  write its own folder — so in that gap the name could be swapped for a link to a file elsewhere on
  the host, which the gateway would then have read and uploaded. Found by review during the
  2026-09-25 QA campaign. The tool first shipped in 0.5.3, and on a gateway using consumer keys (the
  default) no stage could complete before this release, so the gap could not have been used there.
- The assistant no longer suggests a blanket container prune when asked about disk space. The
  rule against `podman system prune`/`reset`, `podman image prune -a`, `podman volume prune` and
  `docker system prune` lived only in the operating guide, and a run that answered without opening
  the guide recommended two of them — on this host they can delete channel homes or the runtime
  image. The rule now sits among the few hard rules every run of every engine reads, pointing at
  `npm run runtime:storage` (removal only with `-- --apply` when an admin asks).

## 0.5.5 — 2026-09-24

- The runtime image no longer carries SSH host private keys. Installing `openssh-server` generated
  `/etc/ssh/ssh_host_*_key`; nothing used them (each channel's `cg-sshd` makes its own key), but
  every channel container on a host held the same three private keys, and the signed release
  would have published them in the image archive. They are now deleted in the step that installs
  sshd. That, plus four reviewed look-alikes — prefix constants in Codex 0.156.1 and in the Copilot
  runtime bundled with the VS Code server, each pinned to its official archive — is what failed the
  `v0.5.3` release evidence scan. Rebuild the image (`npm run build:image`) to drop the keys.

## 0.5.4 — 2026-09-24

- Claude Opus 5.5 and GPT-6 Sol / GPT-6 Luna reach the gateway. The image pins move to Claude Code
  `2.1.281` (2.1.280 added `claude-opus-5-5` and made it the default Opus) and Codex CLI `0.156.1`
  (its hotfix added GPT-6 Sol and Luna to the model catalog). Claude's picker is alias-based, so
  `opus` / `opus[1m]` resolve to Opus 5.5 with no picker change; Codex discovers its catalog live,
  and the bundled fallback list now matches what 0.156.1 serves — GPT-6 Sol and Luna in, the
  withdrawn `gpt-5.3-codex-spark` out. The Codex cost estimate prices `gpt-6-sol` at $2/$0.20/$10
  and `gpt-6-luna` at $0.10/$0.01/$0.50 per 1M tokens, both with the usual 2x/1.5x uplift above
  272K input tokens. Opus 5.5's $4/$20 rates were already in the Claude table. Existing prices are
  unchanged, so no historical repricing runs.

## 0.5.3 — 2026-09-24

- Slack ⚙️ Settings: the VPN moved out of its own **Network & VPN** section and onto the row right
  under the Auto/Lean/**Network** switches it belongs to — `VPN — On / Off / Not configured` plus
  its controls, with the explanatory paragraphs dropped. The separate "Network use" read-out is
  gone for anyone who can already see the network checkbox; members and DMs, who cannot, keep a
  one-line version. A failure or an unavailable service still says why.

- One tool per verb for secrets. `list_secrets` replaces `list_my_secrets` and `list_org_secrets`
  and shows every scope a run receives — organization, personal, this conversation — in one live
  call (names, provider, masked tail, who set them; never a value), filterable with `scope`.
  `set_secret` and `remove_secret` replace the four `*_my_*` / `*_org_*` writers; the scope is an
  argument and the approval card's tier follows it. In an SSH session, `CG_SESSION_ENV` names the
  session's current env file, and the assistant is told to source it in the same command when it
  needs a credential added after its `claude` started — no restart, no reconnect.

- Slack ⚙️ Settings → Skills: the skill template is now a dropdown on the page instead of a
  **Change Template** button that opened a second modal containing that same dropdown. Picking a
  template saves immediately and repaints the page, like the Engine & model selects already did.
  With no templates defined the row stays a read-out pointing at the admin UI. A Settings view
  opened before this change repaints onto the new control, and a template modal that was already
  open still saves.

- Make `claude` over SSH the same Claude a Slack turn runs. A session had no MCP servers, no
  channel secrets and a login labelled "Claude API" with no usage shown, while a turn in the same
  channel had all of it. Now, before your shell starts, the gateway prepares the session like a
  turn: the channel's tool policy and MCP allowlist, the gateway tools, `composio-user` for your
  own connected accounts, `composio-agent` for the channel's, the channel's selected MCP servers
  and its secrets by name — and nothing else, so the operator's own claude.ai connectors never
  load. Claude reads the gateway's login from an access-only file (no refresh token, removed when
  the channel's last session ends) and shows it as the account it is: "Claude Max account", the
  organization, the usage windows and the plan's default model. Everything is refreshed every 20
  minutes while a session is open — and re-prepared within a second when a secret or MCP
  selection is added to the channel, an organization secret changes, or your own secrets or
  Composio token change, so the next `claude` you start in the session has it (a running one
  keeps what it started with, like any process); "show SSH access" says what a session gets.

- Stop Claude Code updating itself inside a channel. Its interactive updater had installed a
  newer copy into the channel's `~/.npm-global/bin`, which is first on the image PATH — so that
  copy silently replaced the pinned CLI for every turn in the channel and hid the gateway's login
  wrapper (the real reason one channel's `claude` still asked to log in after the onboarding
  fix). Every container now runs with `DISABLE_AUTOUPDATER=1`.

- The "reset all channels to gateway defaults" button now asks for the scope. Clearing a channel's
  engine/model only decides what a NEW thread inherits, so every thread that had been pinned by
  hand (`/model` → *just this thread*, or a `claude`/`codex` directive) kept answering on its old
  harness and model and the reset looked like it had done nothing. The confirmation now offers
  **Channels only** (the previous behaviour) or **Channels + threads**, which also deletes those
  per-thread engine, model and effort pins and reports how many it cleared. Per-thread `/clean`
  and `/sudo` postures, DMs and DM threads are untouched either way. The reset also clears each
  channel's reasoning **effort** now: the `/model` wizard picks engine, model and effort in one
  pass, so keeping effort behind left channels on the gateway's default model with a hand-chosen
  effort they were never meant to keep.

- Say why a VPN would not turn on. When the helper refused, the web toggle, Slack Settings and
  the agent all answered "Could not change VPN state", so a VPN image left over from before an
  update looked like an unexplained failure and invited repeated retries. The toggle now gives
  the same fixed diagnostic as the status row, for example that the image needs rebuilding.
  Only a known failure class is passed on; helper text and provider output never are.
- Open interactive Claude signed in over SSH and VS Code. Connecting to a channel's container
  worked and `claude -p` answered, but plain `claude` opened the first-run theme picker and a
  login screen, so it looked signed out. Headless engine turns never finish Claude's onboarding,
  and the flag lives in the channel's `$CLAUDE_CONFIG_DIR/.claude.json`, not `~/.claude.json`.
  A second cause: sshd starts every session with a clean environment, so an SSH shell, a remote
  command and VS Code's server never saw the container's own variables (`CLAUDE_CONFIG_DIR`,
  `CODEX_HOME`, `PATH`, `TZ`, …) and Claude read the wrong settings file. SSH sessions now get
  the same environment `podman exec` gives, every SSH or VS Code attach marks onboarding done
  (merging, never overwriting other settings or a theme you picked), and the gateway's `claude`
  wrapper pins its settings folder. Claude still asks once per folder whether to trust it.

- Start SSH sessions in the channel's folder. An interactive `ssh` login now opens in the
  channel's work folder instead of `/home/agent` (image spec 1.5.1; rebuild with
  `npm run build:image`), and "show SSH access" prints a `code --remote ssh-remote+…` command
  that opens VS Code straight on that folder.

- Show the gateway's own notices in streamed Slack answers. When a turn failed over to the other
  engine, the "Codex hit its usage limit — using Claude" line, the license allowance warning and
  the "Skipped MCP connection" notice were attached only to the finished reply, but Slack writes a
  streamed answer from the live stream — so none of them were ever shown and people could not tell
  which engine had answered. They now lead the answer, and each appears exactly once however the
  reply is delivered.

- Stop the assistant recommending a cleanup command that deletes channel data. Asked whether the
  gateway was running out of disk, it correctly said it could not measure containers from inside
  one — and then suggested `podman system prune -a --volumes`, which removes every stopped
  channel's home volume along with its sessions and logins. The operating guide now forbids
  running or suggesting `podman system prune`, `podman volume prune` and `podman image prune -a`
  by name, and points at `npm run runtime:storage` instead.

- Add `npm run runtime:storage`, a report of what container storage could be reclaimed. It lists
  every channel container, runtime image and volume as keep or remove with the reason, and counts
  the space layer by layer so layers shared between images are not counted twice. It changes
  nothing unless run with `-- --apply`, and it never removes a running container, a channel's home
  volume, the current or previous runtime image, or anything belonging to another gateway install
  whose folders still exist — only leftovers whose folders are gone, such as finished test runs.
  Nothing runs it automatically; run it by hand or schedule the report.

- Share one VS Code server between every channel instead of storing one per channel. Opening VS Code
  on a channel — attach or Remote-SSH — used to download about 650 MB into that channel's home
  volume, per editor version, and nothing ever reclaimed it. The channel image (spec 1.5.0) now
  carries the servers for VS Code 1.139.0 and 1.138.0 once, read-only, and every container start
  links them into the channel, so attaching costs a few kilobytes. Other editor versions still work
  and download their own as before. The build checks each download against VS Code's published
  SHA-256. Rebuild the image (`npm run build:image`) and restart.

- Make rerunning the SSH access installer safe. Without `CG_SSH_HOST`/`CG_SSH_PORT` it used to fall
  back to this machine's hostname and port 22 every time, so a plain rerun — the documented fix
  for several problems — silently replaced a corrected endpoint with the gateway's web hostname
  and put developers back to connections that hang. An unset value now keeps what the previous run
  recorded (and says so); the hostname and port 22 apply only to a first install.

- Stop a second gateway on the same host from silently switching SSH access off. The attach
  socket lives in one directory per host, so a gateway under another runtime directory — a second
  install, or a test daemon started from a worktree — reaches the same path. The daemon used to
  delete whatever was there before binding, and to delete the path again on shutdown even when it
  had never bound it; either way the daemon that owned the socket kept listening on a file that no
  longer existed, so every developer's connection failed while it still looked healthy. It now
  checks the path first and leaves a socket another process is serving alone, replaces only a
  file nobody listens on, takes over within a minute if the other process goes away, and no
  longer removes anything on shutdown except its own socket.

- Stop a hardened host from silently locking every developer out of SSH access. sshd checks
  `AllowUsers`/`AllowGroups`/`DenyUsers`/`DenyGroups` before it looks at any key, and a refusal
  there reads `Permission denied (publickey)` — the same as a wrong key, with nothing in the gateway
  log. A host whose hardening names its real people in `AllowUsers` therefore refused the login
  account for everyone. The installer now asks sshd for the lists it would actually enforce and,
  when an allow-list excludes the account, appends it inside ChannelGate's own `channelgate.conf`
  (the lists accumulate; your file is not edited). It adds nothing on a host with no allow-list,
  where the same line would restrict every login to that account, and it stops with the file to
  edit when a deny list names the account. Rerun `sudo bash scripts/install-ssh-access.sh`.

- **Channel access is edited on the Settings page itself.** Mode, Auto/Lean/Network, who may use
  the channel, who may manage it, named guests and named managers were behind a *Change access
  settings* button that pushed a second modal you had to fill in and submit — five clicks to flip
  one switch. They are now six controls on **General Settings**, each saving the moment it is
  changed, exactly like the engine and model rows above them. Nothing was relaxed: the controls
  render only for admins and channel managers, one change describes one field (the rest of the
  policy is read back from the record, so a repainted control can never resubmit a stale value),
  and every save still takes the membership lock, validates named users against live channel
  membership, re-checks management at the write boundary and audits the change. A rejected pick
  writes nothing. An access form left open from before this shipped says where its controls went
  instead of saving.

- **Slack Settings is five pages behind one dropdown, and the first one is General Settings.**
  Engine & model, Access and Network/VPN were three separate tabs; they are now three sections of
  **General Settings**, in that order, because "what is this channel allowed to do?" was never
  answerable from one of them alone. The row of tab buttons — which wrapped onto a second line as
  pages were added — is now a single *Page* dropdown listing General Settings, Resume Session, MCP,
  Skills and Secrets. Nothing changed about who may do what: the access summary and its editor
  still render only for admins and channel managers, the VPN on/off controls are still theirs, and
  every other control stays open to any authorized user. A Settings view opened before this shipped
  keeps working — its old page ids resolve to the page that now owns those controls. The VPN row
  also stopped costing a subprocess on every open: a conversation with no provisioned service says
  *Not configured* straight away, and only a provisioned one waits for a status read.

- **One worktree per task is now stated as a prohibition, in every place a run reads it.** The rule
  already existed in three files and was still skipped: `references/git-repos.md` opened by handing
  the whole question to the project's own instruction file, and this project's file mentioned
  worktrees only as a clause inside a paragraph about pull requests — so a run that read both
  concluded the protocol did not apply and edited the shared checkout directly. Six unrelated
  features were built that way and left uncommitted, interleaved across the same files, past the
  point where they could be separated into the six commits they should have been. The reference now
  gives a project authority over WHICH branch to base on and land on — naming `beta`/`develop` vs
  release-only `main`, since landing development on `main` is its own incident — while stating that
  the isolation itself is never waived, that a passing mention still requires it, and that "small"
  is not an exemption. The always-on rule says *never edit the shared checkout* rather than
  "isolate edits". This repo's `AGENTS.md` gains the hard rule beside the other invariants and a
  `One worktree per task` section with the actual `origin/beta` commands, the landing gate and the
  cleanup that does not leave a `prunable` entry behind.

- Admin UI → Skills: a search box can be typed into again. The catalog, per-source and usage
  searches filtered on every keystroke, and filtering re-rendered the whole panel — the rebuilt
  input came back focused at its start, so "beta" was typed as "ateb". Searching is now something
  you ask for: type freely, then press Enter, click the new **Search** button beside the box (it is
  highlighted while a typed query has not been searched yet), or clear the box with Escape or the
  browser's own ×. Typing itself only records a draft, which also survives a re-render for an
  unrelated reason, and any render under a focused field now restores the caret where it was.

- Environment secrets now have three scopes instead of one (`src/config/scoped-env.js`).
  **Organization** secrets are injected into every conversation's runs — one `GH_TOKEN` the whole
  deployment shares instead of a copy in every channel — and live in `settings.json` (`orgEnv`,
  0600), outside the `ENV_MAP` that can reach the daemon's own environment. **Personal** secrets
  belong to a person and are injected only into runs THEY authored, in any conversation, never into
  someone else's turn in the same room; they live on the user record beside the personal Composio
  and Toolbox tokens. Precedence is organization → personal → channel, most specific last, so a
  conversation's own account is never displaced by a personal token. The run preamble names each
  variable's scope, the warm-pool fingerprint covers the merged set (a process started for one
  author is never reused for another's turn), an unauthenticated run-API caller gets no personal
  scope, and clean mode still injects nothing. New: `set_org_secret` / `remove_org_secret` /
  `list_org_secrets` (admins) and `set_my_secret` / `remove_my_secret` / `list_my_secrets`; Slack's
  `/secrets` and Settings → Secrets now render all three scopes in one modal (the organization's
  Add button is absent for non-admins, and a row's authority is re-derived from its `action_id`
  rather than trusted from the clicked value); the admin UI's *Organization secrets* card and
  *Personal secrets* in the Users drawer; and `/api/org-secrets` + `/api/users/:id/env`.
  Write-only everywhere, with no reveal path in any scope. One editor implementation now serves
  all three (`public/admin-secrets.js`) and carries its own styling, so a secret box matches the
  canonical admin-UI input wherever it is mounted instead of inheriting whatever its host gave it;
  in the user drawer it stacks full-width like the token fields directly above. `.desc` is now
  small everywhere rather than only inside a `.setcard`.
- Admin UI: the "owner — whose account?" note beside every Composio/Toolbox token field
  (`.tok-label`) now styles itself instead of relying on an ancestor. It was only ever styled where
  it happened to sit inside a `.field` — in Settings — so the same input rendered as a small grey
  browser-default box in the conversation card and the user drawer. It now matches the token field
  it labels in all three. `test/admin-ui-controls.test.js` asserts this class of control carries its
  own box styling, since the bug shipped twice.

- **Overview charts are stacked by model, and there is a chart for the models themselves.** The
  cost, runs and tokens charts now draw one band per model instead of a single line, so a rising
  cost reads as "we moved onto a pricier model" or "we ran more" rather than leaving it ambiguous.
  Attribution is per component, not per run: a Codex turn whose subagent used a different model
  contributes to both, while the run is still counted once — and a run whose components are only
  partly priced contributes no dollars at all, exactly as the headline figure treats it, so the
  bands always add up to the total beside them. A new **Models** card lists every model in the
  window with its token cost, share of spend, runs and tokens. Each chart has a legend and a
  crosshair tooltip that breaks the hovered bucket down per model (pointer or keyboard); colours
  come from a palette validated for the admin surface and are keyed on the model, so changing the
  range, harness or source never repaints the series that survived the filter.
- **Usage that never went through chat is counted too.** The same engines, on the same machine and
  billing account, are also driven by hand — a terminal `claude`/`codex` in a channel's work
  folder, the VS Code extension, the desktop apps, and SSH or VS Code sessions inside a channel
  container. An hourly read-only scan of what both CLIs already write (Claude Code's per-session
  transcripts, Codex's rollouts) now feeds the Overview: an **Outside the gateway** KPI, a **Where
  usage came from** breakdown (chat gateway / terminal CLI / VS Code / desktop app / headless), the
  same per-model stacking, and a **source** filter that partitions the totals into chat-driven runs
  and everything else. Outside work inside a channel's folder is charged to that channel; work
  elsewhere stays in the totals rather than being charged to a conversation that never ran it.
  Nothing is double-counted: every run now records the engine session it spent tokens in, and a
  session the gateway launched — or a Codex subagent whose parent it launched — is skipped before
  its file is opened. Sessions from before that recording started are recognised by WHERE they ran:
  inside a channel container, or in a channel's own work folder, nothing but the gateway runs an
  engine headlessly. That is what lets the scan cover a machine's **entire** engine history instead
  of only what happened after this release, and every pass reports how many sessions it recognised
  as the gateway's own so the outside figure can be checked rather than taken on trust. A container
  is only read when it is already running, through the runtime's existing read-only inspection — a
  usage scan never starts one or makes a turn wait behind it. `npm run usage:external` reprocesses
  on demand (dry run by default, `--rescan` to re-read everything).
- **Fixed: one Claude API response was being charged several times.** Claude Code writes one record
  per content block of a response — same request id, same usage — so summing the records billed a
  single request two or three times. Usage is now counted per request. Checked against the sessions
  where Claude Code recorded its own final cost, the figure this produces now matches Claude Code's
  exactly; before the fix it was 21% high in aggregate and up to 3x on a single session. This only
  ever affected the new outside-the-gateway figures — costs for runs made through chat come from
  the CLI itself and were never computed this way.
- **Updating a gateway now restates its own history.** The outside-usage scan never re-reads a
  transcript it has already seen, which is what keeps it cheap — and what would have left a gateway
  stuck with numbers from an older build for good. Each pass now checks a fingerprint of the scanner
  and of the rate tables against the last completed one; on a mismatch (including rows written
  before this check existed) it forgets its bookmarks once and re-reads everything over the next few
  passes. So deploying an update, or editing a rate table, reprocesses the past by itself — no
  command to run. The per-model breakdown never needed reprocessing: it is computed from the
  existing ledger when the page is drawn.
- **The bar lists are stacked by model too.** Runs per user, Channels and Where usage came from now
  split each bar into the models that produced it, in the same colours and order as the charts — so
  a colour means one model across the whole Overview, and a channel's runs bar and cost bar can be
  compared directly. Also fixed: the ledger and the transcript scan disagreed on how to name a
  dated model snapshot, which showed up as two identically-labelled "Haiku 4.5" rows.
- **Runs with no recorded model are no longer a "model unknown" band.** The gateway only started
  resolving which model answered partway through its life, so older runs carry none — on this
  deployment 387 runs worth $990. They are now charted under a per-engine fallback (Opus for
  Claude, Sol for Codex, both editable in Settings → Agent defaults, blank to keep them unknown).
  Only the model is assumed: the cost is whatever was already recorded, the per-model figures still
  add up to the headline exactly, and the Models card says how many runs in a band were filled in
  rather than measured.
- **Per-model Claude rates** (Settings → Agent defaults). A transcript records tokens, not dollars,
  so outside Claude usage is valued from an editable $/1M table — input, both cache-write TTLs,
  cache read and output, defaults verified against Anthropic's published pricing. Every token class
  is priced separately, because an agent session is mostly cache reads. These rates never touch a
  gateway run: Claude Code reports a real cost for those and it is used as-is.
- Teach the assistant to check container disk usage without acting on it. The gateway never
  reclaims container storage on its own — each image build keeps the previous ~2-4 GB image, and
  the idle reaper stops a channel's container without removing it, so the container keeps its
  image alive. The operating guide now explains how to read free space and per-image container
  counts from an admin `/sudo` thread, how to tell a leftover from a live container (they are
  named by runtime-root hash, and test runs mint their own), and the two traps that make a naive
  cleanup wrong: container tags follow `:latest` so only image IDs identify what is pinned, and a
  removed container leaves its named HOME volume dangling, so a blanket volume prune destroys a
  channel's engine sessions and CLI logins. Deletion stays a manual or scheduled admin action —
  the assistant reports and asks.
- Fix `scripts/install-ssh-access.sh` dying silently under `sudo` when Node is a per-user install:
  root's `secure_path` hid it, and a failing command substitution inside an assignment under
  `set -e` exited before the "node is required" line could print. The installer now looks in the
  invoking user's and the service account's `~/.local`, nvm, volta and fnm trees and the system
  locations (`CG_NODE_BIN` overrides), keeps a root-owned copy of the binary beside the attach
  wrapper so the no-home login account can execute it, proves that before touching sshd, and
  reports the failing line on any abort. It also probes `CG_SSH_HOST:CG_SSH_PORT` and compares the
  answering host key with this machine's: a different key aborts (that address is another server),
  no answer warns that a web hostname behind an HTTP proxy such as Cloudflare never carries SSH.

- **The 💻 button is gone from reply footers; the resume command moved into Settings.** Slack
  replies now end with 📂 Files, 🔑 Secrets, ⚙️ Settings (and any 📄 review-file buttons) — the
  resume control no longer rides under every answer. Channel Settings gained a **Resume Session**
  tab that shows the same copyable `cd "…" && claude --resume <id>` line (the container `exec`
  form for a containerized channel), plus the session id and the folder it belongs to. It is
  resolved from the live session when the tab is rendered, so a Settings view left open cannot
  hand out a cleared session's id, and it names the harness that MINTED the session rather than
  the channel default. `/resume`, `/menu`'s 💻 Resume button and the 💻 control on "🛑 Stopped."
  messages are unchanged.

- **"Just send me the file" now sends the file.** Asking for a file in the thread — "share it here",
  "attach it", "send me the report" — makes the assistant upload it with `slack_upload_snippet`
  under its real name instead of only printing the path and a 📄 button. Any UTF-8 text file
  qualifies: `.md`, `.txt`, `.json`, `.html`, `.yaml`, `.csv`, source, logs. Images keep their
  automatic `![alt](path.png)` upload, `.html` is flagged as downloadable-but-previewed-as-source,
  binaries (PDF, PPTX, XLSX, ZIP) still go through the file explorer, and a file over roughly 1 MB
  is offered rather than pushed through the model's context. The `gateway-usage` skill and the tool
  description both carry the rule, so an engine that never opens the skill still routes correctly.

- **A usage limit is no longer treated as a bug.** Self-diagnosis skips failures that are provider
  account state rather than a defect in this source — usage/spend limits, missing or expired
  sign-ins, billing, a model the account cannot serve, provider outages and dropped connections, as
  well as a user's own stop. Hitting the Claude session limit no longer opens a 🩺 root-cause thread
  in the diagnosis channel (which spent the exhausted quota to conclude "wait for the reset"), and
  no longer burns the 30-minute cooldown a real crash needs. Crashes, stalls and malformed requests
  are diagnosed exactly as before.

- Slack **⚙️ Settings → Engine & model** now edits the runtime in place instead of opening a second
  modal, and covers both scopes: the channel default AND, when Settings was opened from a reply
  inside a thread, that thread's own engine/model/effort pins. Six dropdowns save the moment they
  are picked; an unset field preselects the label of what it inherits (gateway default, org DM
  template, or "Follow channel (…)"), each scope's model and effort lists follow the harness that
  scope resolves to, and a pinned thread gets a **Follow channel default** button that clears all
  three. An unpinned thread whose live session belongs to the other harness is named rather than
  moved silently. The old "Change engine & model" form is gone; a Settings view left open from
  before the change repaints to the new tab instead of pushing it.

- `/model` → **This channel** now changes the thread it was run in, not just threads opened later.
  The pick drops that thread's own engine/model/effort overrides so the new channel values apply
  immediately, and when the thread's live session belongs to the other harness it also pins the
  thread to the chosen one — otherwise the thread stayed on its session's engine and the newly
  chosen model was dropped as foreign to it. The done card says when a thread moves, since the
  move restarts it on the new harness with its earlier messages replayed as context. A thread
  already on the chosen harness (or without a session) is left unpinned and keeps following the
  channel and its cross-engine failover.
- Developers can SSH into a channel's container instead of the gateway host (`docs/SSH-ACCESS.md`).
  A person registers one public key once from chat, a channel manager grants them access per
  channel, and `show_channel_ssh` hands out an `~/.ssh/config` block; the connection reaches a
  dedicated no-shell login account on the host whose forced command hands it to the daemon, which
  authorizes the key, the user and the grant and runs an unprivileged inetd-mode sshd inside the
  container on that stream — pty, VS Code Remote-SSH, sftp and port forwards all land in the box,
  nothing listens on a port anywhere, and a container with a live session is never idle-stopped.
  New: `scripts/install-ssh-access.sh` (root, once), image spec 1.4.0 (`openssh-server`,
  `cg-sshd`; rebuild with `npm run build:image`), migration 27 (`ssh_keys`, `ssh_sessions`), the
  `sshUsers` channel policy key, and six gateway tools (`add_my_ssh_key`, `list_my_ssh_keys`,
  `remove_my_ssh_key`, `grant_channel_ssh`, `revoke_channel_ssh`, `show_channel_ssh`).

- Add a second Anthropic-compatible provider harness, **Qwen EU (Claude Code)**: Alibaba Cloud
  Model Studio's EU (Frankfurt) region, driven through the same Claude Code CLI as the existing
  Qwen harness. It keeps everything the CLI owns — streamed progress, the tool loop, approval
  cards, connectors, `CLAUDE.md`, skills and resume — and serves that region's own catalog, which
  includes the Kimi, GLM and DeepSeek families alongside Qwen. Like the first one it is off until
  you switch it on, never an automatic failover target, and its turns are recorded with tokens
  only.

  The two harnesses are now generated from one provider table, so each has its own settings card,
  its own write-only API key, its own endpoint, its own default model and its own live model list;
  saving one never touches the other. The EU endpoint is per-workspace, so no default is shipped —
  the harness names both missing halves and fails closed until you paste your own endpoint and key.

- Fix two defects the second harness exposed. Naming a harness in a thread matched engine ids in
  registration order, so `qwen-eu` would have selected `qwen` and left `eu` at the front of the
  prompt; ids are now matched longest-first. And the confirmation posted after such a switch called
  every harness either "Claude" or "Codex" — it now names the harness actually selected.
- Let a conversation hand a file it generated to something outside Slack. Until now a run that
  produced a PDF or an export had no way to deliver it anywhere: Composio's file-taking tools
  (Drive upload, Gmail attachments, Slack uploads) accept only bytes already staged in Composio's
  own storage, and nothing in the gateway could put them there — so the file stayed in the
  channel's container while the agent explained why it could not be moved. `stage_file_for_composio`
  now performs Composio's documented upload flow from the daemon and hands the model the exact
  object those tools expect. You name which identity will run the upload, and that identity's key
  is the one spent; a key never enters the container and nothing is published.

- Added temporary public file links for the cases a URL is the only way in — an API that ingests by
  URL, or a person who just wants a download link. `create_public_file_link` serves one file from
  the channel's own working folder at a short, unguessable URL. A machine-facing link lives five
  minutes and allows five fetches; a person-facing one lives exactly as long as you say, up to a
  hard 48-hour ceiling, and needs an Approve click naming the file and the duration. Links are
  listable and revocable, every fetch is audited, and the whole capability is off until an admin
  turns it on in Settings → Public file links — where turning it back off kills every outstanding
  link at once. A link can never reach outside the channel's working folder, including on an Admin
  channel that mounts the operator home.

## 0.5.2 — 2026-09-22

- Give a conversation its tools back when one engine runs out of quota. A channel whose engine had
  hit its usage limit was answered by the other harness, as designed — but when the fallback was
  Claude it started with no MCP servers at all: no gateway tools (channel memory, schedules,
  Slack history, approval prompts), no Composio, no Cloud MCP selections, and no channel
  permission lockdown. Tool requests were denied with no approval card instead of asking. The turn
  still replied, so the only visible symptom was the agent reporting that Composio and its own
  gateway tools were missing. Every fallback turn now spawns with the same MCP payload, lockdown
  and approval route a normal turn gets. Clean mode still injects nothing.

- Stop the channel VPN's health check from getting the database address blocked. Every 30 seconds
  it opened a MySQL connection and dropped it before logging in; MySQL counts that as a failed
  connection and, after about 100 of them, refused the tunnel outright (error 1129, "host
  blocked"), so database reads failed within an hour. The check now verifies only the tunnel and
  its route. Rebuild the VPN image (`npm run vpn -- build --channel ID`) and turn the VPN off and
  on; a database that already blocked the address still needs `FLUSH HOSTS` once.
- Sync Google Drive on demand. A channel's admin page has **Sync now** beside **Test**, Settings →
  Google Drive sync has **Sync all now**, and asking the agent to sync Drive runs the channel's
  linked folder immediately through the new `sync_channel_drive` tool. Each shows when the pass
  finished or why it failed, and `get_channel_drive_folder` now reports the last sync. Manual
  syncs obey the same enable switch and never double a pass already running.

## 0.5.1 — 2026-09-21

- Fix two release-smoke defects. A Qwen run with no model configured now asks QwenCloud for a
  Qwen model instead of the CLI's Anthropic default (which failed every such turn with "Model not
  exist"). A tool request from an HTTP run-API turn is now refused with a readable reason instead
  of Claude Code's "invalid permission result" error; the request stays denied either way.

- Stop an unusable Cloud MCP selection from failing a conversation. A selected optional MCP server
  that is missing from the host configuration, needs a host credential, or no longer matches the
  transport it was selected with is now dropped from that run — for Claude, Qwen and Codex, on the
  primary and the failover engine alike — instead of ending the turn. The answer is prefixed with
  which connections were skipped and why, and the drop is recorded as an event for admins. Host
  credentials are still never relayed into a channel container, and a corrupt operator
  configuration no longer blocks every channel that selected anything.

- Add an optional **Qwen (Claude Code)** harness: the same Claude Code CLI driven against
  QwenCloud's Anthropic-compatible endpoint, with its own gateway-level API key and base URL, and
  a model list read live from the account. It is off until an admin turns it on, after which it
  appears in the admin engine selectors, the Slack channel settings modal and `/model`. No
  Anthropic credential ever reaches a Qwen run, it is outside the automatic failover in both
  directions, and its turns are recorded with real tokens and no invented dollar cost.

- Run prepared channel VPNs with OpenVPN 3 Linux and refresh their protected supervisors on ON.
  Add channel-scoped, bounded read-only database operations for Claude and Codex, without granting
  ordinary agent containers VPN privileges or exposing database credentials.

- Provision the host-side `rclone` dependency during fresh deployments and repair it during an
  update even when the checkout is already current. User services install it in `~/.local/bin`,
  downloads are checksum-verified, and Google Drive connection tests retry an earlier missing-tool
  result without requiring a daemon restart.

- Control a prepared channel VPN through the agent, the web channel Network controls, and Slack
  Settings → Network. Managers/admins can turn it on/off; status distinguishes connecting from
  connected and reports safe certificate/authentication errors. Stopping also cleans up manual starts.

- Move quiet-thread reminders from conversation settings to a personal user preference. Each
  reminder now follows and mentions the requester across channels and DMs; users can turn it on or
  off in Slack App Home, while admins can manage individual or organization-default choices.

- Add an optional operator-managed OpenVPN/MySQL service per channel, with dedicated tunnel
  privileges, database-only routing/firewall, protected channel-secret references, a persistent
  user service and read-only verification. Ordinary chat containers retain their existing rights.

- Fix Codex capacity refusals delivered as plain-text `turn.failed` events. They are now classified
  as transient provider failures, retried in place, and automatically handed to the other enabled
  harness when capacity remains unavailable.

- Add an organization-admin-only `/sudo` posture for individual Slack threads. While enabled,
  admin messages and their background work execute directly on the gateway host as the daemon OS
  user; non-admin messages are rejected as sudo-thread traffic before work starts. `/sudo off`
  restores the normal per-conversation container, with Claude/Codex session state carried across
  the boundary when possible. The host runtime cannot be selected through channel metadata or the
  run API, and every turn rechecks current admin authority.

- Render up to five public images referenced in an agent's Slack answer as native Block Kit image
  previews, while preserving clickable Markdown fallbacks and completed text delivery when Slack
  rejects a preview. Images referenced from the channel workspace are now uploaded into the thread
  as native Slack files instead, so they get the same inline thumbnail, download control and full
  preview as a human attachment.

- Highlight every conversation that shares its resolved working folder with another conversation
  in red in the Admin UI, and warn in red while browsing a folder that is already assigned elsewhere.

- Remove gateway-downloaded audio after a successful local or Slack-fallback transcript, while
  retaining failed inputs for retry and refusing symlinks or paths outside managed uploads. Teach
  the built-in video-understanding workflow to remove only successfully processed uploaded source
  videos after all required re-sampling, never project files or failed inputs.

- Correct Codex history repricing to honor OpenAI's effective date for GPT-5.6 Sol: retain the
  original $5 / $0.50 cached / $30 rate before 2026-08-21 and apply $4 / $0.40 / $20 from that
  date onward. Upgraded instances rerun the backup-first correction under a new pricing basis.

- Refresh Codex Standard API-equivalent pricing from official OpenAI documentation: add
  GPT-6 Astra at $10 / $1 cached / $50 per million tokens and reduce GPT-5.6 Sol (plus its
  `gpt-5.6` alias) to $4 / $0.40 / $20. Keep the fallback picker aligned with the current Codex
  CLI catalog, leave CLI-only Spark explicitly unpriced until an official rate exists, and
  automatically back up and reprice component/request history since 2026-07-13 once on upgrade.

- Fix Slack question-card posting by encoding channel membership checks as GET query parameters.

- Let agents ask clarification questions with Slack cards and paged forms: custom option buttons,
  Yes/No, multiple selections, and written answers. Save drafts until submission, retain pending
  questions across restarts, and continue the requester's thread after they submit.

- Keep sidebar update messages inside the rail, wrapping long details and showing a short commit
  revision with the full hash on hover.

- Add System health to the admin UI with real CPU/RAM/load and storage metrics, historical
  charts and peaks, storage warnings and capacity estimates, and refreshed hardware inventory.

- Fix MCP configuration selection when a plugin package supplies both engine manifests. Each
  engine uses its own inline declarations or referenced files, avoiding wrong endpoints and
  duplicate server errors.

- Reply in Slack when conflicting shared-folder skill or memory settings prevent a request
  from starting, with admin repair instructions instead of silently logging the error.

- Put Remove and Add/update directly on Slack Settings → Secrets. Refresh the same page after
  deletion, and return saved entry forms to their parent without duplicate lists or modal stack growth.

- Remove the `/files` slash command and typed file-browser entry. The reply folder button and
  Browse channel files shortcut remain available; legacy typed requests explain the replacement.

- Explain in the All Shared Skills hint that its summary updates automatically when its source
  groups are configured, matching the other combined skill summary.

- Include the current advisory network policy in every engine attempt, including resumed and
  Clean turns, so a changed switch supersedes stale context and cached request results.

- Preserve existing channel guest grants when an unrelated Admin UI setting is saved, even if
  the current member roster omits a saved guest. Explicit guest edits retain membership validation.

- Include received human approval in completed control-plane tool results while preserving the
  operation outcome; clarify that approval cards can be outside the model transcript and that
  the receipt records an explicit human decision, not an approval supplied by channel Auto mode.

- Keep subsequent turns working after an author deletes a personal skill, while retaining its
  grant and revision history for restoration and refusing foreign personal skill access.

- Correct skill-creation approval descriptions to show personal versus shared ownership,
  optional channel grants and automatic personal/channel-scope activation accurately.

- Keep text-only gateway commands on their control path after thread file sharing, while
  preserving canonical current attachments and ordinary agent attachment recovery.

- Reject incomplete engine turns even when partial text exists; preserve failure diagnostics and
  usage. Reserve restart recovery before Slack reconnects, show each recovering thread promptly,
  and retry pending recoveries on reconnection without requiring another daemon restart.

- Fix one-time automation edits shifting by the browser/daemon timezone difference and potentially firing future tasks immediately.

- Manage plugin packages through existing skill sources, review, templates and channel grants.
  Compile native Claude hooks as an inline event map so SessionStart hooks execute correctly;
  keep executable hooks restricted to authorized live admin turns.

- Fix repeated native file creation and inline editing exhausting Slack’s modal stack. Successful
  forms return to the existing browser/preview; Cancel and validation retain their prior behavior.

- Fix the first browser editor/upload opening from Slack: narrowly scoped SameSite=Lax session
  cookies survive the cross-site GET redirect while preserving CSRF, authorization and cookie
  confidentiality protections.

- Correct the bundled skill-authoring workflow: updates and change proposals accept only changed
  files while retaining omitted files; complete packages are required for initial creation.

- Correct Composio guidance that assumed the agent identity always belonged to other people or
  was shared across every channel. Preserve account selection and no-substitution rules, including
  when personal and agent connections reach the same underlying service owner.

- Fix missing run identities in flat chats. Group-chat messages start separate sessions; quoting
  user or bot messages resumes the original session, including after restart. Teams quotes support
  both current entities and legacy Reply markup; personal chats remain continuous.

- Make skill assignment rows compact, reveal descriptions and metadata on click, and give Active
  skills and Add skills matching fixed heights with preserved scroll position when managing skills.

- Make existing channel API/CLI credentials discoverable through a current names-only inventory
  on non-Clean attempts, including resumed turns. Teach the operating guide to check credentials
  alongside skills, CLI logins and selected-account MCPs before requesting another connection;
  clarify runtime environment use without exposing values or silently changing accounts.

- Improve conversation and template skill selection with a shared Active skills / Add skills
  editor, name and source filters, locked inherited groups, and preserved explicit selections.

- Clarify the Slack Skills categories and where each is configured. Org Skills explicitly
  states that only admins can add or remove organization skills; combined lists are labelled
  as automatic summaries.

- Save instruction-update approvals with no deadline. The exact rule remains actionable across
  restarts and is applied once on approval; changed instructions, destinations or permissions
  require a fresh request. Other live engine permission/plan approvals retain their timeout.

Next release version selected by the owner. Development continues on `beta`; full testing and
explicit approval of the exact candidate are required before promotion to `main`. Earlier
0.6.0 candidate tags and evidence below are retained as historical snapshots, not published releases.

- Give selected optional Codex MCPs the existing 120-second remote startup budget and raise
  Claude's default initialize timeout to the same ceiling. Preserve explicit operator overrides,
  tool-call deadlines, grant boundaries and quiet-run liveness behavior.

- Persist Codex's native thread ID as soon as the CLI announces it, so stopping a first turn
  offers a valid resume control. Late announcements cannot restore a cleared session.

- Correct channel-control MCP descriptions and confirmations to reflect container confinement,
  advisory networking and the separate optional operator-home mount. Preserve all permissions,
  setting mutations and approval checks.

- Show only supported scopes for explicit agent and durable approvals. Reject unsupported
  thread/forever decisions before consuming the request or persisting ineffective grants;
  native tool permissions retain all three scopes.

- Retire completed warm Claude pacing sessions so native timers cannot duplicate daemon-owned
  loop ticks; queued messages resume safely and ordinary turns retain warm reuse.

- Inject explicitly selected Claude MCP transport definitions into isolated runs without enabling
  ambient settings. Fresh configuration reads and warm fingerprints apply additions, changes and
  revocations to resumed threads; unavailable or credential-dependent definitions fail closed.

- Resolve optional Codex MCP launch definitions from effective configuration when native tool
  status omits them, while rejecting credential-bearing transports and retaining fail-closed
  selection when no safe definition is available.

- Keep existence/access-only requests within their requested scope: both engines receive
  per-attempt guidance to use non-mutating metadata, avoid content reads and temporary write
  probes, and leave unverified write access explicit.

- Report failed reads of explicitly configured skill source folders as sync failures while
  retaining the last approved revision; a recovered source clears the error on successful sync.

- Explain deliberately omitted connectors, skills and memory from each attempt's resolved Clean
  mode. Distinguish the host gateway runtime store from container-local engine files and mount
  scaffolding using actual mount coverage, without disclosing unmounted host paths.

- Include recorded user/channel attribution and an exact time window in chat skill-usage
  reports, with one usage total and honest inferred-read provenance.

- Show current and projected context costs alongside the soft-cap warning when granting
  channel skills through chat or the admin UI/API, while keeping activation advisory.

- Deliver resolved home-access facts directly on every engine attempt, including resumed threads
  that retain stale isolation claims. The guide and prompt share one note; injected environment
  credentials are described as usable with masked listings and redacted output.

- Correct the operating guide's unconditional denial of host access in Full-access channels.
  Generated guidance now reports the resolved operator-home grant and global switch, while
  preserving the default container boundary, channel-wide read scope and admin-author tool bypass.

- Supply the running assistant with its current engine, configured model/effort and session
  state on every attempt, so model-identity answers can use the same configured selection as
  the footer. Recompute after retries, recovery and fallback; never infer a provider model
  from an alias or an unexposed harness default.

## 0.6.0-rc.2 — 2026-09-08 (superseded draft)

Follow-up candidate for the installer findings from disposable Linux acceptance. Full live QA
remains deferred; stable promotion is not approved.

- Provision rootless images with a clean service-account environment and from the installed
  application directory, so operator storage settings and an inaccessible invocation folder
  cannot break a dedicated-account installation.
- Export the release image with pipeline failure propagation and verify gzip integrity before
  generating its checksums and attestations.
- Recognize exact reviewed public vendor fixtures during release-artifact scanning, including
  complete self-test/development keys and compiler-adjacent constants. Unknown values still fail,
  and repository files and Git history receive no exceptions.
- Add disposable Linux installation, encrypted backup/restore, real updater rollback and actual
  OS reboot checks; authenticated engine and chat acceptance remains a separate live gate.

## 0.6.0-rc.1 — 2026-09-08 (superseded draft)

Candidate source and build evidence for review. The GitHub Release remains a draft; the planned
full live QA campaign is pending. This is not approval for stable-release promotion.

- Add a manager/admin-only Access page to Slack channel settings for base mode (including Admin), Auto, Lean,
  network, use/manage policy, and named guest/manager lists. Recheck live membership and current
  roles on save; immediately acknowledge submissions while validation runs. Admin-author-only
  engine bypass and existing ordinary-tab access remain unchanged.

- Channels: use Read-only, Worker, and Admin as the three base modes, with independent Auto and
  Lean side toggles in channel, DM, and template editors. Slack Settings can now select Admin
  for authorized admins. Admin gives other members Worker plus their selected options; Auto
  approves tools for both roles, while Lean applies only to non-admins in an Admin channel.
  Existing Full access records now get the promised Worker fallback.

- Repository cleanup: remove unused Gemini/Hermes instruction aliases and the superseded working
  audit. Keep canonical agent instructions, project records and active release acceptance gates.

- Settings: replace the Testing with AI checkbox list with a compact searchable user selector,
  removable selection chips, keyboard navigation and a bounded results list that fits narrow screens.

- Settings: add **Testing with AI**, an opt-in Slack user list for supplemental approval and
  busy-thread browser links. Empty by default; both auto and always link modes respect the list,
  including for admins. Native buttons and non-native approval surfaces keep their behavior.
- Users: show enabled personal skill counts in the table and widen the editor to 640px,
  with taller skill lists and a stacked layout on smaller screens.

- Skills: restore Source and Category catalog filters alongside governance filters. Sources now
  use clickable cards opening a searchable skill list with persistent Enabled, organization-wide
  Discoverable and Mandatory switches, including disabled skills. Source configuration is grouped
  in collapsible settings.

- Dependency compatibility: update mcp-remote to 0.8.3 in both daemon and image, Slack Bolt to
  5.1.0 and Zod to 4.5.4. Keep Composio 0.14.0 for the supported Node 22.13 floor. Nightly
  pinned engine probes now read the actual image manifest, preventing version drift.
- Public project policy: license v1.4 uses Cluj-Napoca venue; trademark registration is deferred;
  direct requests use contact@makeitfuture.com and contracted support requires an active
  Makeitfuture or approved-partner agreement. Existing published releases retain their terms.
- Scheduler restart recovery checks due work immediately and persists its cursor and cron-minute
  claims. A restart spanning the due minute now catches up within a five-minute window without
  replaying claimed work, backfilling before creation/re-enable/cron edits, or rerunning uncertain
  engine effects. Queued tasks resume, saved results redeliver, short transport outages defer
  unclaimed recurring work, and scheduled failure notifications show a sentence instead of JSON.
  Reminder claims remain at-most-once across an ambiguous remote-post crash boundary.
- Keep mount rebuild waits visible and cancellable until active occupants finish, then continue
  automatically; retain preparatory leases through rebuilds and avoid concurrent waiter deadlocks.
- Recover the narrow unsupported Codex list-turns resume response, prevent automatic continuation
  after ambiguous Claude hard kills, and retain structured exit/signal/runtime diagnostics.
- Recognize natural-language loop Stop, cancel loops before Slack delivery waits, record per-turn
  Stop outcomes, and release stream cleanup after a bounded grace while preserving partial text.
- Render numeric progress counts in replaceable titles and normalize secondary provider failures.
- Codex now receives usable personal skill instructions through a per-turn catalog alongside
  native shared skills. Ephemeral files include references, remain outside shared project skills,
  and are cleaned up after the run. Auth/session homes stay unchanged; missing selected personal
  grants fail visibly. Personal catalog delivery does not register native slash commands.

- Codex reads child identities and token accounting inside the channel runtime, including resumed
  turn baselines. Rootless HOME volumes no longer need to be readable by the daemon. Named child
  rows and elapsed/token totals use the same reducer without new mounts or an image rebuild;
  inspection failures are visible instead of silently reporting an empty child list.

- A thread whose first message died before its engine ever started no longer stays stuck on
  that engine. The session row is minted, with its harness stamp, the moment a new thread is
  resolved; a turn that then failed closed on a pre-spawn gate (a missing Claude login, a runtime
  that could not come up) left the stamp behind, so every later message in that thread "continued
  on" a harness that never produced a session — and failed the same way even after the channel had
  moved to Codex (live, 2026-09-05, a fresh gateway with only a Codex sign-in). A row minted by the
  failed turn itself is now dropped (deleted, not tombstoned — the next message is a true first turn
  again, resolved from the channel default); an existing thread's session is never touched, and a
  per-thread harness pin is a separate store and still wins.
- Admin UI: Automations rows use fixed timing and last-run columns so the titles line up down a
  card.
- Admin UI: the Pending approvals rows wrap the command being approved (monospace, a few lines)
  instead of running it underneath the Approve/Deny buttons; the Automations search box uses the
  same styled search component as the Users page, with a clear button and Escape to clear.
- A model the provider refuses no longer answers as a different model in silence, and never
  answers in JSON. Two halves of the same live failure (a channel whose model id did not exist):
  the gateway did substitute its default and logged the substitution, but the ⚠️ note was only
  prepended to the finished reply text — and a natively streamed Slack answer is written from the
  live stream, so the thread never mentioned that a different model had answered. The note is now
  announced to the delivery layer before the retry spawns, so it leads the streamed answer, on both
  the channel's own harness and a cross-engine fallback whose own model is refused; it is still
  carried on the reply text for surfaces that have no stream, and delivered exactly once either
  way. On the Codex side the same misconfiguration dead-ended instead: that CLI reports some
  provider refusals by handing back the whole HTTP response body as its error message, which the
  classifier — reading status and error type from the event, not from the document inside it — saw
  as an unclassifiable failure, so the turn failed after minutes with the raw JSON posted in the
  thread. The runner now unwraps that body before classifying (same fallback, same visible note),
  and any failure that still reaches the thread is rendered as a sentence — the provider's own
  words, never a document — with a model rejection naming the model and the remedy (`/model`, or
  the admin UI).
- Loop guidance: the schedule tool's minimum-interval refusal now points at the thread's native
  loop pacing for short repeats, the loops reference says a sub-floor repeat is never a background
  agent or an in-turn sleep, and a loop's stop reply must state the iterations run and the pending
  tick dropped.
- Full-access channels have their shell back. The admin-run settings file is the one an admin
  author's escalated turn actually loads, and it was derived by copying the shared file and
  removing a single key — so it inherited read mode's "every Bash command asks for approval" rule
  while the same turn spawned with the permission bypass and therefore had nowhere to send an
  approval to. Claude Code still evaluated the rule: a bare `pwd` came back denied, a command with
  a variable in it was refused outright, and the turn announced that the shell was not actually
  granted and continued read-only. Because the Full-access preset turns on admin mode alone (it
  never sets "allow shell"), that hit every channel using the preset as shipped. The admin variant
  now grants the shell tools outright and carries no ask rule, and it is generated by the same
  function as every other variant instead of being patched after the fact. Nothing widens: the file
  is only ever handed to an admin author in an admin-mode channel, whose turn already bypasses
  permission prompts. The shared file of the same channel is untouched, so a non-admin author there
  still gets an approval card for every command.

- `create_schedule` now tells the model that `notify` and `delivery` are opt-in: a plain "remind
  me" keeps the defaults (@channel ping, result threaded under the Running announcement). One
  engine had been choosing `delivery:'channel'` + `notify:'none'` unprompted, so the scheduled
  result landed top-level with no thread and no ping.
- A finished answer is never held back by Slack's temporary status line. The assistant-status
  writes used to queue without bound on one serial chain, and because each write sleeps its own
  retry-after while the workspace is rate-limited, the terminal clear ended up behind a backlog
  thousands of calls deep — and finalize waited for it BEFORE posting the reply, so turns that had
  finished were delivered tens of minutes later or not at all. The status queue now keeps one write
  in flight and one pending slot holding the LATEST phase (a superseded phrase is dropped, never
  sent late), and delivery waits at most a few seconds for the clear before going ahead; the clear
  still runs afterwards.
- A Slack-ended answer stream no longer costs the rest of the reply. When Slack completes a
  message's stream server-side (a window blown by rate limiting, or its undocumented age cap), the
  first refused append used to abandon the answer: every later delta was dropped and the reply was
  left ending mid-word, with no footer, under a red "Something went wrong". The answer now gets the
  same republish the progress card already had — a fresh stream carrying the complete compiled
  answer, the footer on the surviving message, and the stranded copy removed only once the
  replacement is durable. A failed finalization still ends with exactly one complete, footered
  answer.
- Every run that injects Composio now says WHICH identities it received, in the prompt itself: one
  line naming `composio-user` and/or `composio-agent`, prepended beside the fresh-session memory
  catalog and the caller's provenance note. With both present it restates the ask-first stop ("which
  account?"); with only the shared one it says a request phrased for the requester cannot be served
  there. The rule already lived in the managed instructions block, but the block is a shared file
  and could not name a per-author fact — one harness answered a bare "check the calendar" from the
  shared identity and posted someone else's week into the channel. A channel with no Composio gets
  no line, and forging the line in replayed thread text is defanged like the other framing markers.

- Codex subagents get their own progress-card rows again. `codex exec --json` never puts a child's
  identity on stdout — a turn that spawned two subagents sent one anonymous coordination item and
  nothing else — so the card showed a single generic `wait_agent` row where Claude showed a row per
  child. The Codex runner now reads each child's own session record for its name, opens a row while
  the children work, and closes each row with the child's elapsed time and token spend.
- The "🛑 Stopped." card's resume command now names the harness the stopped thread was actually
  running on, resolved per thread (per-thread override, then the engine that started the thread's
  session, then the channel's engine, then the gateway default). A session belongs to one harness,
  so a thread running the non-default one used to be handed a resume line the other CLI rejects.
  `/resume` and the `/compact` passthrough check use the same shared resolver.

- Addressed the release audit: isolated runtime credentials and daemon metadata, tightened SDK/API
  authorization and admin reauthentication, serialized memory saves, and separated interrupted
  execution from result-delivery recovery. Google Chat intake is durable and bounded; non-Slack
  automation no longer requires Slack connectivity.
- Composio SDK is now **Enterprise-only, Beta**. Google Chat and Microsoft Teams are **Beta**.
- Runtime hardening: editor leases live in daemon-owned state, container bind sources reject
  symlinked path components, shell background jobs no longer inherit engine service credentials,
  and service credentials are redacted from replies and job output. The Claude login relay and
  the shared Codex sign-in mount are unchanged.
- License 1.3 clarifies no-key/free-key use of unchanged bundled EE enforcement and authorized
  redistribution. Provider agreements and legal review remain separate release requirements.
- Patched transitive qs, corrected fresh-service provisioning, and expanded release history scans,
  built-image inventory/model hashes and signed artifact evidence. Public instructions no longer
  require private contributor accounts; unused generic skill/UI scaffolding was removed.


- Automations are searchable by conversation/person or automation content, resolve DM display
  names, and describe common schedules in plain language. The enlarged editor now updates timing,
  title, notifications, delivery, state, and prompt atomically, including a new direct-to-channel
  task delivery alongside per-run and per-day threads.
- A progress card whose Slack stream Slack itself ends — an append delayed past its window by
  rate limiting, or an age cap the local rollover clock did not beat — is republished into a fresh
  stream instead of being abandoned. Slack renders an abandoned stream as a bare "Something went
  wrong", so a turn that recovered from a failed tool call and answered correctly was left with a
  red error banner sitting above its answer for the life of the thread. The replacement carries the
  whole toolbox (warning rows included), is made durable before the stranded copy is deleted, and
  the terminal seal recovers the same way. A failed tool row is still only a row label; `error`
  remains reserved for a stage the agent itself declares failed.

## [0.5.0] — first public release (2026-09-06)

> **Versioning.** 0.5.0 is the first published version of ChannelGate; the repository became public
> on 2026-09-06. The internal iteration labels used below the first public entry (the container
> runtime work labelled "v0.8", and the June 2026 "v1.0.0 (foundation)" milestone) were names for
> private milestones, never publications — the public version series starts here. Everything under
> this heading is what 0.5.0 contains, accumulated since the foundation.

### Highlights
- Container-per-channel runtime (rootless Podman, Linux + systemd only) as the sole confinement
  boundary, with a per-channel HOME volume, image-shipped toolchain and durable channel state.
- Claude Code and OpenAI Codex as interchangeable harnesses with per-thread choice, transient
  failover, warm sessions and the operator's own login relayed into every run.
- Slack in full (threads, streaming progress, approvals as buttons and as links, native charts,
  Lists, file explorer and editor, attachments to 500 MB), with Microsoft Teams and Google Chat
  transports behind the same capability-declared platform layer.
- Skills platform: local catalog, channel and personal tiers, templates followed live, Git and
  folder sources with review, publishing, a catalog MCP endpoint and peer-gateway sync.
- Uncapped channel memory with on-demand retrieval, schedules, background jobs, follow-ups,
  video understanding, VS Code attach, and an admin UI for all of it.
- Licensing: free tier with limits, license keys issued by the platform at channelgate.dev,
  Ed25519-signed entitlements with a 14-day offline grace, and privacy-bounded usage reports.


### Added
- **Approvals as links, so every chat surface can answer one.** Native buttons are Slack's
  primitive: Teams and Google Chat have none the gateway drives, and automation cannot click
  anything at all. Every approval card (permission, control-plane, durable background-shell) and
  every busy-thread card is now ALSO minted as short-lived, single-use, HMAC-signed URLs — one per
  action the recipient may actually take — and delivered **privately to the person who raised the
  request**: a Slack ephemeral in the same thread, a DM on a surface with no ephemeral primitive,
  never the shared thread (a link is a bearer credential). `GET /approve/<token>` renders a
  confirmation page and changes nothing, because Slack, Teams and corporate proxies prefetch and
  unfurl links; `POST` — the page's single Confirm button — resolves the request through the same
  `applyApprovalDecision` / `applyBusyThreadChoice` the buttons and the admin API use, so scope
  semantics, the durable compare-and-swap, the waiting MCP call and the card update in the thread
  are identical to a click. The waiting agent reads `decided_by: "link"`. The signature covers id +
  action + scope + expiry (a *Deny* link cannot be edited into an *Approve forever* one), the nonce
  is spent by one atomic UPDATE (a link works exactly once), deciding a card retires its other
  links, and the requester's own authority is re-checked on every POST — so an admin-tier sign-off
  is handed a Deny link and no Approve link. Unknown/used/expired answer 404/410 with a page that
  reveals nothing about any other request, bad tokens get per-IP backoff, and every response is
  `no-store` + `noindex`. New setting Settings → Connection → **Approval links**: `auto` (default —
  where there are no native buttons, plus Slack once a public URL is set), `always`, `off`. Every
  link decision logs `approval_resolved_by_link` (ids, decision, scope — never a value). Slack's
  buttons are unchanged.
- **Approvals can be resolved from the admin UI and over HTTP, not only by a chat click.** A
  pending approval used to be answerable only by clicking its card in a real Slack client, which
  blocked every automated and QA path that has to get past a permission prompt, a control-plane
  sign-off or a durable background-shell request. `GET /api/approvals` now lists what is waiting
  (conversation, requester, tool, a clipped command/plan preview, age, expiry — no values),
  `POST /api/approvals/:id` takes `{ decision: "approve" | "deny", scope?: "once" | "thread" |
  "forever" }`, and `POST /api/approvals/thread-choice/:id` takes `{ choice: "steer" | "queue" |
  "cancel" }` for a busy-thread card. The decision half of the button handler was factored into one
  shared applier both callers use, so scope semantics, the durable compare-and-swap, the waiting MCP
  call, the requester binding and the in-thread card update are identical either way. Unknown or
  expired is 404, already decided is 409, and every resolution logs
  `approval_resolved_by_admin` with the principal and decision (never the command). Overview shows
  the same queue with Approve/Deny buttons. Admin session only — the run-API key cannot reach it.
- **Full-access channels can see the whole gateway home.** A new gateway-wide switch in Settings →
  Container runtime (`containerFullAccessHome`, off by default). While it is on, every channel in
  Full access gets the gateway user's whole home directory bind-mounted read-write at its identical
  path inside its container — every channel's work folder and memory, every repository under that
  home, the gateway root with its logs, metadata and credential stores — with only the container
  engine's own storage masked out (a write into a running container's layers would corrupt it;
  the mask is a `notmpcopyup` tmpfs — podman's default would copy the whole store into it).
  That is an overseer channel: one agent that can see every other agent and every repo, without
  leaving the container. Until 2026-09-02 admin channels ran on the host, "honestly unconfined";
  containers-only (2026-09-03) confined them to their own work folder; this restores "admin sees
  everything" as an explicit operator choice. It is a boolean, never a path; it is part of the
  container's create-time fingerprint (recreate at the next turn, HOME volume intact); no MCP tool
  can flip it; and it is per channel — every admitted author can read the home through the file
  tools, only an admin author's turn writes with the bypass tools. Claude's admin-run settings
  variant lists the home in `permissions.additionalDirectories`, derived from the resolved mount so
  the file can never name a path the container does not have; Codex admin turns already bypass
  their sandbox. Other Linux users' homes stay unreadable (no `sudo` in the image) and system
  directories are the image's own.
- **Deterministic personal/shared Composio discovery.** Both bundled skills now define
  `composio-user` as the active requester's identity and `composio-agent` as the shared agent
  identity, select them by pronoun or connection alias, ask when an app is connected to both,
  recognize underscore-normalized MCP tool prefixes, and verify connection status before reporting
  an app unavailable—without silent fallback or initiating a connection during inventory.
- **Source-specific Skills Git credentials.** GitHub catalog sources now use their own optional
  write-only token, while publishing has a separate write-only token and follows `main`. The Add
  source dialog offers only GitHub or Other ChannelGate, reveals type-specific fields, and takes a
  `/tree/main/<path>` URL instead of separate branch/subfolder controls.
- **Usage-led skill governance.** Skills now opens on the top 20 skills used in the last 30 days,
  Templates is second, usage displays one harness-neutral total, and Catalog defaults to usage
  order with category/source filters plus source-aware description search. Per-skill Enabled,
  Discoverable, and Mandatory controls govern source imports; mandatory skills load everywhere.
  Templates contain explicit skills only, agents can search by source and admins can change the
  governance flags through `set_skill_governance`, and Overview charts the top ten skills.
- **Skill repository sections.** The skills repository is one shared library plus one folder per
  channel (`channels/<channel id>/`); a skill in a channel's section is granted to that channel
  automatically. New skills go to the library by default; `create_skill` takes `scope: "channel"`
  for customer-specific ones (the agent asks first), and `set_skill_scope`, the Skills view's
  *Section* control or the admin API move a skill either way — files move in the repository, and a
  promoted skill stays with its former channel as an explicit grant. `.` as the publish folder
  means the repository root.
- **The bundled ChannelGate skill now matches the product.** Its small entrypoint routes to focused
  references for containers/security, write-only secrets and durable CLI/device logins,
  Claude/Codex credentials, MCP identities and the local skill platform, catalog-first channel
  memory, conversation operations, and manual folder/headless configuration. Package validation
  guards frontmatter and progressive-disclosure links. Memory search/read registration also keeps
  the gateway text-response adapter in scope, fixing the live `text is not defined` failure.
- **Video understanding is part of the main gateway skill.** `gateway-usage` now ships the complete
  synchronized visual/audio workflow, sampling and dependency guidance, and local analyzer script
  to every conversation. The former standalone catalog skill is excluded and removed from durable
  grants at boot, while stale gateway-managed copies are pruned on the channel's next turn.
- **Live Slack progress no longer splits the answer.** Native task/tool/thinking rows stream in a
  dedicated first message, while the Markdown answer streams contiguously in a separate message
  beneath it. Both streams retain their independent long-run rollover and terminal/fallback
  handling; text-only turns still remain a single answer message.
- **Uncapped channel memory with on-demand local recall.** Markdown remains canonical, fresh
  sessions receive only a compact catalog, and new channel-scoped search/read tools use a derived
  SQLite FTS5 index. The former 8,000-character write ceiling and prompt-body injection are gone.
- **Conversation Settings now follows the operator workflow.** Access emphasizes the three normal
  modes and separates Full access/Lean; Tools has Connections, MCP servers, Environment tokens,
  and Skills categories; inherited guest access is visible; enabled skills sort first.
- **The Skills admin workflow is organized around the operator's task.** Sources use an Add source
  modal; synchronization/publishing and MCP access have separate tabs; templates have a searchable
  selection editor; and Usage has searchable By skill / By channel views, compact bars, detection
  provenance, and plain-language context-warning explanations.
- **Organization-wide local video analysis.** The shared conversation-container image now includes
  `ffmpeg`/`ffprobe`, pinned OpenCV and faster-whisper, plus a pre-cached Whisper `small` model, so
  the built-in `gateway-usage` video workflow works without per-channel installs or first-use downloads (image
  spec 1.2.0).
- **`slack_download_file` — an agent can fetch a file shared earlier in this channel.** The pre-run
  downloader only ever delivered the files on the triggering message, so a recording posted on a
  thread's first message was unreachable from a later "try again" reply and the bot could only ask
  for a re-upload. The new gateway tool fetches a file by id (from `slack_channel_history` /
  `slack_thread_replies` metadata or a pasted Slack link) with the bot token into the current
  thread's `uploads/` folder and returns the local path — fail-closed to files Slack reports as
  shared in THIS channel, the same 500 MB cap and streaming writer, a file already in the folder
  reused. Alongside it, a thread ROOT's attachment is carried into a later reply automatically and
  downloaded while its bytes are still missing from the folder (never re-attempted when its
  declared size is over the cap — that refusal was already reported at the root).
- **Inbound attachments up to 500 MB, streamed to disk.** A Slack, Google Chat, Teams or Run API
  (`fileUrl`) attachment may now be up to 500 MB (was 30 MB, or 25 MB for the Run API); the one
  shared cap lives in `src/util/bounded-bytes.js`. The bytes no longer pass through the daemon's
  memory: every sink streams the body straight into the channel's `uploads/` folder through an
  exclusive no-follow temp file with the running total checked per chunk, and renames it into
  place only when complete. A refusal names the actual size against the limit (`263.4 MB exceeds
  the 500 MB attachment limit`) instead of `too large`; a large Slack download announces itself
  in the assistant status before the run starts.
- **The installer builds the channel image.** `npm run setup` now runs `npm run build:image` after
  the dependencies and the Whisper step, so a fresh gateway has the whole shared toolchain
  (engine CLIs, `ffmpeg`, OpenCV, `faster-whisper` + model) before its first message instead of
  failing the first run closed. `--skip-image` / `CG_BUILD_IMAGE=no` defers it; a failed build is
  reported with the manual remedy and never aborts the install.
- **Skill templates are followed live, and edited in Settings.** A conversation is assigned a
  template (Conversations → Tools → *Skill template*, the Skills view, `set_channel_skill_template`,
  or a DM template) and gets the template's current skills plus whatever is added to the
  conversation itself; editing the template under Settings → Access Templates → *Skill templates*
  (a catalog checklist plus categories) reaches every conversation that follows it. Replaces the
  one-time copy of the day before.
- **Skills platform, round two: Skills Manager migrated in, Git publishing, an MCP endpoint of its
  own, peer gateways.** Skills authored or approved in chat are pushed to a configured GitHub
  repository (one commit per file) and adopted by the matching source; a GitHub push webhook
  triggers a debounced sync; `POST /mcp/skills` serves the catalog to laptop Claude Code, Codex and
  any MCP client with scoped, revocable access tokens (the `library_*` tool names kept); a source
  of kind `gateway` lets a second gateway follow another's library; personal (author-only) skills,
  self-service personal grants (`add_my_skills`), feedback proposals, `delete_skill`, organization-
  tier and source administration from chat, compatibility notes, and
  `scripts/migrate-skills-manager.mjs`, which moves a deployment's Skills Manager repositories,
  favorites, team favorites, exclusions and personal skills into the catalog.
- **Skills platform (Core): the gateway owns its skill library.** A local catalog in the gateway
  database stores every skill a conversation can be granted as immutable, content-hashed revisions
  of the exact bytes of every file (`SKILL.md` included), with explicit ownership per slug
  (bundled / host folder / git source / local), tombstones instead of deletes, pins for rollback,
  and a review queue. Conversations get token-free skill profiles: the existing organization →
  conversation → user grants resolve against the catalog with `requires:` dependencies, a context
  cost estimate, and real files materialized write-on-change (Claude via its plugin, Codex via
  `.agents/skills`) — the Skills Manager stub and its mid-turn fetch are no longer needed for
  catalog skills. **GitHub sources** (Skills Manager's tarball sync ported and hardened: branch
  names with `/` resolve correctly, review vs auto mode, commit pins, last-good state on failure)
  and host folders feed the catalog; **templates** (Development / Sales / Marketing / Management,
  editable) apply a snapshot of skills to a conversation; **chat verbs** cover listing, granting,
  templates, `create_skill`, `update_skill`, proposals with admin approval (an approved change to a
  source-owned skill becomes a pinned local override), usage reports and source sync; **usage
  telemetry** records exact (Claude `Skill` tool) and inferred (Codex `SKILL.md` read) signals and
  lists never-used grants; the admin UI gains a **Skills** view (catalog, review, sources,
  templates, usage). `src/gateway/skills/`, `src/mcp/tools/skills.js`, `src/web/routes/skills.js`,
  `public/admin-skills.js`, migration 14, `docs/SKILLS.md`, `gateway-usage` → `references/skills.md`.

### Removed
- **The Skills Manager runtime integration.** Skills now come from the gateway's own catalog as
  files, so the `makeitfuture-skills` MCP injection, the organization/channel/user Skills Manager
  tokens (with their tools, routes, admin UI fields and secret readers), the favorites stubs and
  the App Home favorites fetch are gone. Stub folders left in older channel folders are pruned on
  the next message. Skills Manager itself remains a standalone product; Toolbox is unchanged.

### Changed
- **Skill exclusions stick.** A skill an admin removes from the catalog (the Skills view,
  `set_skill_excluded`, a Skills Manager exclusion carried over by the migration) now stays out when
  its source delivers it again; before, the next sync silently restored it. Only a skill its source
  dropped still comes back on its own. Restore brings an excluded skill back on its newest revision.
- **Production license verification is live by default.** The gateway now ships the Ed25519 public
  key published by the ChannelGate licensing platform and uses `https://channelgate.dev` as
  its default platform URL. Fresh installs can verify platform-issued keys without an environment
  override; `CHANNELGATE_LICENSE_PUBLIC_KEY` remains available for staging and coordinated rotation.
- **Small text files now offer both editing surfaces.** When a Public URL is configured, eligible
  files up to 3,000 characters retain the native Slack *Edit* popup alongside *Edit in browser*;
  larger eligible files remain browser-only. Slack does not expose a modal-size setting, so the
  browser editor remains the larger workspace.

### Security
- **The approval-link and admin-login backoffs count the real client, not the tunnel in front of
  it.** The daemon binds `127.0.0.1` and is reached from outside through cloudflared or a reverse
  proxy on the same host, so every public caller arrives on a LOOPBACK socket — and both per-IP
  backoffs keyed on that address, which made them one bucket for the entire internet. A handful of
  bad approval tokens from anywhere put every legitimate approval link into 429 (verified live: a
  429 raised through the public URL also refused a link opened from the machine itself), and the
  same shape let one stranger's failed password guesses lock the admin out of the login form —
  exactly the outcome the backoff exists to prevent. Both limiters now key on the FORWARDED client
  through one shared helper: `CF-Connecting-IP` (which the tunnel overwrites, so it cannot be
  prepended to), then the first hop of `X-Forwarded-For`, honoured **only when the socket itself is
  loopback** — from a non-loopback socket those headers are whatever the client typed and are
  ignored, unless the operator declares a trusted proxy elsewhere on the network with
  `CG_TRUST_PROXY` — and only when the value parses as an address, so a header can neither pick a
  bucket for someone else nor fill the limiter's map. Express's app-wide `trust proxy` was
  deliberately not used: it would also re-point `req.secure` / `req.protocol` / `req.hostname` at
  client-supplied headers, which the DNS-rebinding Host/Origin guard and the session cookie's
  `Secure` flag read for themselves on purpose, and it knows nothing about `CF-Connecting-IP`. New
  runbook section in `docs/OPERATIONS.md` → *Reverse proxies and tunnels*.
- **"My inbox" is never answered from the shared Composio identity.** A requester with no
  personal Composio token asked, in their own words, for their own mailbox. The run had only the
  shared agent identity (`composio-agent`) — which holds OTHER people's connected accounts — used it
  without saying so, and reported a third person's email address and recent subject lines as the
  requester's. Ambiguity was already a hard stop; substitution was not, because the rule against it
  lived only in the `gateway-usage` skill body, which one harness never opened (the other refused
  correctly). The always-injected managed block now carries it as a hard rule: a request phrased for
  the person asking ("my inbox", "my calendar", their own name) is served ONLY by `composio-user`;
  when `composio-user` is absent from the run or has no connection for that app, the answer is to
  say so and stop — never a read of the shared identity, "not even to check" — and the mirror holds
  too ("your X" never touches the requester's identity). The block stays inside its 4 KB budget.
- **Channel policy changes are audited, and a refused secret reveal leaves a trace.** Two gaps in
  the audit trail, both confirmed live. (1) A channel's meta record IS its security posture, and
  changing it wrote nothing: turning *Allow network* off, repointing the working folder and
  switching a conversation to Full access through `PUT /api/channels/:channelId/meta` all persisted
  with zero rows in `events` — only the per-channel environment secrets were audited — and the chat
  twins (`set_channel_admin_mode`, `set_channel_network`, `set_channel_bash`, `set_channel_auto_mode`,
  `set_channel_workdir`, `set_channel_drive_folder`, the MCP allowlist tools, the typed `/mode`
  command and the `/model` runtime picker) wrote nothing either. Every one of them now emits a
  single `channel_meta_changed` event naming the conversation, the principal (`admin-ui` for the
  admin UI's one shared password, the chat author's own id for anything typed or clicked in Slack)
  and the POLICY keys that actually moved, with their before/after values. The diff is one helper
  (`src/config/channel-audit.js` → `policyDiff`) shared by every surface, over a curated allowlist —
  so a token, a per-channel environment value or any other non-policy field can never reach an audit
  row, an unchanged key writes nothing, and a save that touches no policy key (a nudge toggle, a
  re-submitted form, a token rotation) writes no event at all. Skill grants made from chat now write
  the same `skill_granted` / `skill_revoked` / `skill_template_assigned` rows the admin UI already
  did. (2) `POST /api/secrets/reveal` answered 400 for any field off the reveal allowlist BEFORE it
  logged anything, so someone holding a stolen session could sweep the endpoint for revealable field
  names — probing `adminPassword`, `__proto__`, every config key they could think of — and leave the
  audit completely empty; only a wrong password logged (`secret_reveal_denied`). A refusal now logs
  `secret_reveal_rejected` with the requested scope/field/id NAMES (clipped, never a value) before
  the 400. All three outcomes of that endpoint — granted, wrong password, refused field — now also
  name the admin principal (`admin-ui`, the same spelling the skills audit has always written; the
  UI's sessions carry no personal identity); they used to be written with an empty author, so an
  audit row read as if nobody had asked for the secret. The revealed value is still never logged.
- **The admin UI shows the events trail.** `GET /api/audit/events` existed but nothing rendered it,
  so every audit row above was invisible to an operator. **Activity** now carries an *Admin &
  security events* table under the run history — time, event, conversation, who, and what changed
  (for a policy change: `key: before → after`) — defaulting to the admin/security kinds with a
  toggle for the full feed and 25-row pagination. An unrecognized kind still renders, with its raw
  name opened up, so a newly added event is readable the day it ships.
- **A retired integration's token is no longer served in cleartext by the admin API.**
  `GET /api/channels` masked the Composio, Toolbox and Make toolbox secrets by name and then spread
  the rest of each channel's stored record into the response. `skillsToken` — the Skills Manager MCP
  token, dead since the local skills catalog replaced that integration — was in neither set: no code
  read it, so no masker named it, and every channel whose stored meta still carried one handed the
  full value to anyone who could reach the admin API. **Operators should rotate any Skills Manager
  token that was ever stored on this deployment**, in the issuing system; the gateway itself no
  longer uses it, so nothing needs re-entering afterwards. Three changes, because masking alone was
  what failed: every channel read (the channel list, the DM list and the save response) now goes
  through the ONE masker instead of three drifted copies of it — which also closes the DM listing's
  unmasked Make toolbox key; the dead field is stripped from every record on the way into the store
  and on the legacy JSON import; and a schema migration deletes it from the channel and user records
  that already hold one, so the value stops existing rather than merely stopping being displayed.
  The users listing was never affected — it is built from an explicit allowlist, which is now the
  documented reason it is written that way. Retiring an integration means adding its field to
  `src/config/dead-fields.js` in the same change.

### Fixed
- **A busy-thread approval link names the conversation, in the page and in the audit row.** An
  approval card is minted with its conversation slug, but a busy-thread card is built from a raw
  chat event and carries only a channel id — so the *Conversation* row on its confirmation page
  read `C0123456789`, a string the person deciding has never seen, and every
  `approval_resolved_by_link` row for a steer/queue/cancel decision was written with an empty
  `slug`, which is the field the other approval events are queryable by. The slug is now resolved
  from the channels index (the same place the admin approvals list reads it) once per request, in
  the shared resolution both verbs use, so the page and the audit row always agree.
- **A shell turn that reads two skills in one command now records both.** Codex has no Skill tool:
  a skill is "used" when the run reads its `SKILL.md`, and the whole shell line arrives as one tool
  event. The matcher stopped at the FIRST `…/skills/<slug>/SKILL.md` in that line, so the routine
  shape `sed -n '1,240p' …/gateway-usage/SKILL.md && sed -n '1,320p' …/<granted>/SKILL.md` recorded
  the always-on guide and silently dropped the granted skill the turn was about — two identical
  runs of the same skill, one row. Every match in the text is now recorded (deduped per run), and
  the path pattern accepts a read through either skills directory (`.claude/skills`, the
  `.agents/skills` symlink Codex follows), absolute or relative, quoted, `~`-relative, or anywhere
  inside a compound command, while still refusing look-alikes such as `myskills/…`.
- **The Usage panel explains how it counts.** The report has always carried the notes that make its
  numbers readable — exact (Claude's Skill tool fired) versus inferred (a shell read of `SKILL.md`,
  best effort), and that capture is not retroactive — and the admin UI dropped them on the floor, so
  a total with no context looked like a measurement rather than a lower bound. They now render as a
  help line under the Usage header. One total per skill is unchanged.
- **An over-the-cap warning reaches the person who caused it.** Granting a skill resolves the
  conversation's whole always-on profile, warnings included ("always-on skill descriptions cost
  about N tokens per turn (soft cap M)", a skill still awaiting review, a missing dependency) — and
  both grant paths computed them and threw them away. Whoever pushed a conversation over the cap was
  the one person who never heard about it; the warning surfaced only later, in the admin Usage
  panel, to somebody else. `POST /api/skills/profile/:channel/grant` now answers with `warnings` and
  `contextTokens` (resolved over the conversation's whole durable tier: organization + template +
  its own grants) and the admin UI appends them to the grant confirmation; `add_channel_skills`
  appends them to its chat reply and reports the same durable-tier cost it warns against.
- **The Container runtime card describes the runtime the gateway actually has.** Its copy still
  offered containers as an alternative to "the daemon's host sandbox" and promised that "admin-mode
  channels always stay on the host" — both untrue since the containers-only change: there is no host
  sandbox, and an admin-mode channel runs in its own container with the permission bypass applied
  inside it.
- **The template skill search filters again, and the Add-source dialog shows one kind of field.**
  Both hide elements by setting the `hidden` property, and the admin stylesheet was quietly
  overriding it: an author `display` declaration beats the user-agent sheet's
  `[hidden] { display: none }` whatever its specificity, so `.skills-picker-row { display: flex }`
  and `.field { display: block }` kept every row and every field on screen. Typing in Skills →
  Templates' skill search filtered nothing at all, and choosing GitHub or Other ChannelGate in the
  Add source dialog left both kinds' fields visible at once. Both classes now carry a `[hidden]`
  companion rule.
- **A skill pinned to an older revision describes THAT revision.** The `skills` row's frontmatter
  columns (name, description, category, tags, requires, version) are a derived index rebuilt from
  the newest ACTIVATED revision, so a rollback — or the pinned override an approved proposal writes
  — kept advertising the newest revision's name and description beside the pinned revision's files,
  in the catalog table, the skill detail, the chat listings and the peer-sync manifest. Every
  catalog read now overlays the pinned revision's own `SKILL.md` frontmatter, exactly as the file
  list already followed the pin; an unpinned skill is returned untouched.
- **A template preview says what the conversation will actually pay in context.** The always-on
  estimate counted only the template's own skills and their dependencies, while the organization
  tier loads in EVERY conversation whatever the template says — with a few dozen mandatory skills
  configured, the number an admin sizes a template against was short by most of the real cost.
  `preview_skill_template`, `set_channel_skill_template` and `GET /api/skills/templates/:slug/preview`
  now report labelled numbers instead of one: what this tier adds, what the organization tier costs,
  and the effective union (dependencies included, a skill in both tiers counted once), measured
  against the configured context soft cap.
- **Saving a channel environment secret no longer claims the conversation card has unsaved edits.**
  The card marks itself dirty on any input or change inside it, and the Environment tokens pane is
  inside that card — so typing a variable name or value, and storing it with its own request, left
  an "Unsaved changes" bar hanging over a card with nothing to save. Controls that save through
  their OWN request (a write-only value must never round-trip through the card's Save) are now
  exempt in all three dirty-trackers: the conversation card, the DM/template card and the Settings
  page. Nothing else about the bar changed.
- **A container whose workspace moved is rebuilt before the next turn, never reused.** A channel's
  work folder was pointed at a subfolder, used for a few turns, then restored and the subfolder
  deleted. The warm container had been created with that subfolder bind-mounted as its workspace;
  the create-time fingerprint mismatched, but the rebuild was DEFERRED as "runs are active" and
  three more turns were exec'd into the container anyway — each dying on `Append system prompt file
  not found: …/CLAUDE.md`, because the directory it was bound to no longer existed. Two things were
  wrong. Containers now carry a second label, `cg.mounts`, covering only the create-time inputs that
  decide what the container can SEE (work dir, clean workspace, artifact dir, HOME volume, every
  bind and mask): when that half of the fingerprint moves, the turn never runs against the old
  mounts — it waits, bounded and announced in the thread, for the runs still inside to finish and
  then rebuilds, or fails with a message naming the pending rebuild. A mismatch that is only about
  behaviour (a rebuilt image, a limit, the network mode) is still deferred to the next idle moment
  exactly as before. And the deferral no longer fires spuriously: the asking turn takes its own
  container lease before the environment is brought up, and that lease was being counted as "a run
  is active inside", so every turn looked busy to itself — a caller now passes its own lease handle
  and the backend asks whether anyone ELSE is inside.
- **Saving Settings no longer reverts what someone else changed.** The Settings page's single Save
  re-submitted the WHOLE form from the snapshot the page had loaded with, so any value written
  after that load — by a second admin, by the skills sync, by a license write, by the first-boot
  password upgrade — was silently reverted by an unrelated save minutes later (a restored
  `channelTemplate.effort` lost to a save of `scheduleMaxPerChannel` is what surfaced it). Save now
  sends only the fields that actually differ from what the page was painted from, and the server
  merges them exactly as it always did — a field nobody touched is not in the request and cannot
  revert anything. On top of that, `/api/settings` carries a `settingsVersion` that a save may echo
  back: if anything wrote settings in between, the save is refused with 409 and NOTHING is written
  (the check runs inside the same lock as the merge, so it cannot be raced), and the refusal
  carries the current settings — the page repaints from them and names the keys that moved, so the
  admin re-applies their change on top instead of over. A caller that sends no version (an older
  UI, a script) is merged in exactly as before. A repaint also clears anything left pending — an
  armed *clear* toggle, a typed password or service-account key — because a pending action captured
  as "already saved" would silently never run.
- **A channel environment variable typed in lower case is the variable you meant.** The admin card
  sent the name exactly as typed and the store rejected anything but `A–Z0–9_`, so `supabase_token`
  was a validation error instead of `SUPABASE_TOKEN`. The name is now upper-cased visibly as it is
  typed (and on blur, and before it is sent), and the store folds case itself, so the Slack modal
  and any API client get the same canonical name — an update of an existing variable rather than a
  refusal or a duplicate. Nothing else is forgiven: a dash, a space or a leading digit is still
  refused (quoting what was typed), and the reserved-name check runs on the folded name, so `path`
  cannot smuggle `PATH` past it.
- **A pre-SQLite upgrade no longer loses every channel's lockdown record and thread→session map.**
  The one-time JSON→SQLite import listed the legacy channel slugs from `channels/`, then read each
  one's `meta.json` / `sessions.json` through the path helpers — which moved with the per-platform
  folder rename and now resolve to `channels/<platform>/<slug>/`. On an authentic legacy tree those
  files are at `channels/<slug>/`, so the import found none of them: it logged `meta=0 sessions=0`
  and completed successfully, and the upgraded install came up with every channel's capability
  settings and every thread's engine session gone. Nothing failed; the data was simply not there.
  It cannot be avoided by migrating the folders first, either — the boot migration reads its channel
  records from the store, which opens the database and runs this import, before it moves anything.
  The legacy paths are now spelled out, with the current layout accepted as a fallback for a
  half-migrated tree; the import stays one-time and still leaves every source file byte-identical.
- **An expired licence stops granting its tier.** A licence whose `expiresAt` had passed was routed
  into the "platform unreachable" grace lane, which keeps the last verified tier for 14 days from
  the last successful check. For an OFFLINE (air-gapped) payload that check is stamped at read time,
  so it always looked freshly verified and the 14-day window never closed: an expired enterprise
  payload kept reporting tier `enterprise` with unlimited conversations indefinitely, and the run
  gate admitted accordingly. Expiry is now resolved before the unreachable lane, as its own `expired`
  state: the no-key limits apply from the moment the licence runs out, no tier is reported, and the
  admin card says on which date it expired. An expiry date is not a network condition — unlike a
  platform outage it is known in advance — so it gets neither the 14-day window nor the month-boundary
  courtesy, exactly like `invalid`/`revoked`. The genuine case those courtesies exist for is
  unchanged: a still-in-date licence that could not be re-checked keeps its tier for 14 days and then
  until the next UTC month boundary. A licence with no end date, or an unreadable one, never expires.
- **A daily-thread schedule starts a fresh session on every fire again.** A scheduled run's session
  key was built from the message its result is threaded under, which is a different message per
  fire only for `standard` delivery. `delivery:"daily-thread"` reuses one anchor for the whole
  server-local day, so the day's second and later fires landed on the same key and RESUMED the
  previous fire's engine session — carrying context between runs the schedule contract explicitly
  promises are independent. The session key is now derived per fire and never from the delivery
  thread; the anchor still groups the day's results exactly as before. Known limitation, unchanged
  and now documented in the code: the scheduler starts only after restart recovery finishes and its
  first tick lands a minute later, so a cron matching a minute that passed during boot is skipped
  until its next match — firing that minute would need a durable per-minute fire record, because
  the in-memory one cannot tell it apart from a minute that already fired before the restart.
- **A Claude run's skill uses now land on the skill they name.** Claude names a plugin-provided
  skill as `<plugin>:<slug>` in its `Skill` tool call, and the usage recorder looked that whole
  string up in the catalog — it matched nothing, so every exact signal Claude produced was stored
  as an off-catalog name with no skill or revision id, and the usage report marked the skill it had
  just fired as not in the catalog. The recorder now strips a leading `<plugin>:` prefix before the
  lookup, so a plugin-qualified use attributes to the catalog skill (and its effective revision)
  exactly like a bare one; a plugin skill the catalog does not know is still recorded, under its
  bare slug so repeats aggregate together.
- **The audit feed's attachment count is what the turn actually received.** `run_start` logged the
  number of files on the Slack event, not the filtered set that reaches the engine, so a reply whose
  only attachment was carried in from the thread root and is already on disk logged `files: 1` while
  nothing was downloaded. It now logs the filtered count and, when the event carried root files at
  all, a `carried` count beside it, so the difference is visible instead of misreported.
- **The guide no longer promises a bare `stop` works in a channel.** `/help`, the `/stop` reply, the
  README/INSTALL notes and the guide's loop and table references all said to "type `stop` in that
  thread"; in a channel the mention gate drops an un-mentioned message before the stop word is ever
  read, so only `@bot stop` (or a 🛑 reaction) stops a channel thread — a bare `stop` works in a DM.
  Every one of those sentences now says which is which. The gate itself is unchanged.
- **A bounded repeat-check is a schedule, not an in-turn poll loop.** The managed block's
  "only the gateway can report back" rule named the durable tools but left "check every 10 minutes,
  6 times" ambiguous, so a run could read an in-turn sleep loop as compliant while the turn is held
  open. The rule now says a finite repeat-check is `create_schedule` (or `run_agent_in_background`
  for a self-contained watcher) and never an in-turn sleep/poll loop, a `Monitor`-style wait, or a
  harness background task — even when the loop would finish inside the turn.
- **The rules a run must never get wrong are now in the context every run receives.** Guidance that
  lived only in the `gateway-usage` skill reached one engine and not the other: the skill body is
  read when the model chooses to open it, and across a retest wave of failing transcripts the string
  `gateway-usage` appeared only in the skills catalog listing while none of the rule text appeared at
  all — the other engine, reading the identical text through the `AGENTS.md` symlink, followed it and
  passed. The cost was not stylistic: a personal calendar read into a shared channel without asking
  whose it was, an "inventory" call that quietly raised a fresh authorization request, remote
  execution run against the requester's own identity unasked, and follow-ups promised on background
  processes that die with the turn and never report. The gateway-managed block at the top of every
  conversation's instruction file — appended to the system prompt of every run, in every mode, clean
  mode included — now carries a compact **Hard rules** section beside this conversation's switches:
  the two Composio identities and the stop that makes an unnamed request a question rather than a
  tool call, that `COMPOSIO_MANAGE_CONNECTIONS` initiates connections for any action (`list`
  included) rather than listing them, and that only the gateway's `run_in_background`,
  `run_agent_in_background` and `create_schedule` can report back after a turn ends. It is
  engine-neutral, applies wherever the named tools exist, and the gateway-owned part of the block
  stays under 4 KB so the always-on prompt weight is read rather than skimmed; the full reasoning,
  tool shapes and examples stay in the skill.
- **A Codex thread keeps its Composio tools on every turn, not just the first.** The second message
  in a Codex thread came back without `composio-user` or `composio-agent` at all — the registry
  showed only the `gateway` family, so Workbench and every connected app vanished mid-conversation
  while the cold turn had them. Neither the argv nor the CLI was at fault: `codex exec resume`
  carries and honours the same `-c mcp_servers.*` overrides a fresh `codex exec` does. Codex simply
  does not BLOCK a turn on MCP startup — it takes whichever servers have finished by the time it
  builds the first request, and `startup_timeout_sec` only caps the handshake instead of extending
  that wait. A fresh run assembles instructions, skills and workspace state first and leaves
  roughly two to five seconds of room; a resumed run reaches the request in about two. The two
  Composio servers were bridged to stdio through `mcp-remote`, which needed ~2.4s to answer
  `tools/list` (two Node bootstraps, an OAuth-discovery round trip that a static-header server
  never needs, then a duplicated initialize) — inside the cold window, outside every warm one,
  while the local gateway server always made both. Header-bearing remote MCP servers (both Composio
  identities and both toolboxes) are now dialled by Codex itself over its native streamable-HTTP
  transport, which answers in about 1.2s, and their credential is produced by a per-run
  `http_headers_helper` script that reads the run's existing 0600 secret bundle — so the token is
  still absent from Codex's argv and environment, and never reaches the shell snapshots Codex
  writes into the channel's home volume.
- **Read-only conversations no longer run shell commands without asking.** A read-mode channel said
  "no shell" only by leaving `Bash` out of the lockdown file's `permissions.allow`, and that is not
  what Claude Code enforces: for a simple command whose argv head is on the CLI's own built-in
  read-only list (`id`, `cat`, `head`, `tail`, `wc`, `stat`, `strings`, `readlink`, `uname`, `df`,
  `diff`, …) it answers "allowed" before it ever consults `--permission-prompt-tool`. So a cold
  read-mode turn executed `id -un` inside the conversation's container in a tenth of a second — no
  approval card, no approval record — and could read any file that container user can read,
  sidestepping the folder scoping the Read tool applies. Every conversation that does not grant the
  shell (read, lean, and the shared file an admin conversation's non-admin authors run under) now
  names `Bash` in `permissions.ask`, which is evaluated ahead of that layer: every command, simple
  or compound, routes to the approval card, which is what Read mode always promised. Conversations
  that DO grant the shell are untouched (an `ask` rule outranks `allow`, so listing it there would
  card every command they exist to run), the narrow `MEMORY.md` write grant is untouched, and an
  admin author's escalated turn still bypasses prompts through its own settings variant.
- **Codex subagent work is visible on the task card again.** The mapping built child rows only from
  a collab tool call's `agents_states`, and Codex's multi-agent v2 mode reports children somewhere
  else entirely: the spawn arrives as a `collaboration` function call, the child's start and finish
  as `SubAgentActivity` items, and the only collab tool call it emits — the wait — carries no
  receivers and no states at all. A turn where two children really ran therefore showed nothing.
  Every shape is now read: `agents_states`, `receiver_agents` and `receiver_thread_ids`; the
  `SubAgentActivity` lifecycle, keyed on the child's THREAD id so its start (the spawning call id)
  and its finish (a synthetic completion id) merge into one row; and the spawn call itself, whose
  requested task name titles the row while its encrypted message never can. Item types match in
  both spellings the CLI uses (`collab_tool_call` in the exec stream, `CollabAgentToolCall` in the
  session stream), and a collab call that names no child now renders the coordination step itself
  instead of nothing. Note the remaining CLI limit: under multi-agent v2 (CLI 0.152/0.153)
  `codex exec --json` forwards no per-child lifecycle at all, so such a turn shows its wait step;
  per-child rows appear wherever the CLI reports child identity.
- **Codex answers no longer run their segments together.** Codex reports each assistant message as
  its own completed item — a stage narration, then the answer — and the native Slack stream shows
  exactly what the runner streamed, so the last word of one segment was glued to the first word of
  the next ("…isolates conversations.ChannelGate isolates each…"). A new message item now opens a
  new paragraph; deltas inside one streamed item are still concatenated untouched (the break marks
  a boundary, it does not reformat prose), and the authoritative final message read from Codex's
  `-o` file is unchanged.
- **A skill's dependencies are no longer stored as grants of their own.** Granting a skill through
  the chat verbs, the admin API or a newly authored skill wrote the RESOLVED `requires:` closure
  into the tier's stored grant list, so the dependency reported as a direct channel (or personal,
  or organization) grant instead of *required by* its parent, showed up checked in the
  conversation's skill list, and stayed behind as an orphan grant when the parent was revoked. A
  grant list now holds only what was explicitly granted; dependencies keep being resolved at every
  materialization and in every effective profile, so nothing that used to load stops loading, the
  skills and usage views attribute them to the skill that requires them, and revoking a parent
  takes its dependencies with it. Granting a dependency by name is still an ordinary grant that
  outlives its parent. The grant surfaces now report the dependencies a grant pulls in, and a
  revoke of something that is only a dependency says which skill keeps it active instead of
  claiming a removal that changed nothing.
- **The Allow-network switch is now a two-state fact everywhere, and honest about being advisory.**
  The mode label appended a network suffix only when the switch was ON, so "off" and "nobody ever
  configured it" rendered identically — in `/mode`, in the channel list, everywhere. `/status`
  carried no network state at all, and the engines were told nothing either way: asked whether it
  was allowed on the network, a run could truthfully answer that neither its system context nor its
  channel instructions nor its session config mentioned it. The label now states the switch in both
  directions (`Bash · network off` / `Bash · network on`), `/mode` and `/status` add the caveat
  (`network off (advisory — not enforced by the container yet)`), and the gateway-managed block at
  the top of every conversation's instruction file — the file both harnesses read — states the mode
  and the network switch to the engine itself. The honesty matters: under the container runtime the
  switch is advisory (every container is on the bridge network and no egress is policed per
  channel), so a run told "you have no network" would call the switch broken the first time a
  request succeeded. The stale header in `src/engines/network-policy.js` claiming "off" ran the
  container with no network at all is gone, and every `run_config` event now records
  `networkEnforced: false` beside `networkPolicy` so an operator reading it after an incident
  cannot mistake `"off"` for "this turn could not reach the internet".
- **The agent no longer promises follow-ups nothing will deliver.** Claude Code's own
  `Bash(run_in_background: true)` is reachable inside a gateway turn and was being used to say
  "I'll report back when it finishes"; Codex faked a watch loop with sequential sleeps. Both die
  with the headless turn, so the report never came. The bundled `gateway-usage` guide now names the
  harness's own backgrounding (`run_in_background: true`, `nohup`, `at`, `screen`/`tmux`) and
  in-turn sleep loops as dead ends, states that only `run_in_background`, `run_agent_in_background`
  and `create_schedule`/loops survive the turn, and requires the agent to say plainly that it
  cannot follow up when the channel's mode allows none of them.
- **The skills docs no longer promise an auto-mode bypass that does not exist.** `docs/SKILLS.md`
  and the guide's skills reference said the skill chat verbs show an approval card "unless the
  conversation is in auto mode". Auto-approval covers tool permission prompts only: control-plane
  changes (`add_channel_skills`, `set_channel_skill_template`, …) always post a card and block for
  a click, which is the product contract. Both now say so, and the reads
  (`list_skill_templates`, `preview_skill_template`, `show_channel_skills`) are correctly
  described as open.
- **Stopping a turn now stops everything the turn started.** A container run was signalled by
  process GROUP, and Claude Code's Bash tool puts its shell in a session and a process group of its
  own — so `kill -- -<leader>` reported success, the tool's shell survived, and a loop it was
  running went on to completion minutes after the turn was reported stopped. `cg-signal` now walks
  `/proc` once before it signals anything and takes down every process in the run's session plus
  the leader's whole descendant tree (deduped by process group, then by pid), keeping its "0 when
  something was delivered" status; `cg-sweep` signals through the same helper so a boot sweep and a
  stop can never drift apart. Nothing outside the run can be in either set: the leader is its own
  session leader and a detached background job is a separate leader with its own session. Image
  spec 1.2.1 — `npm run build:image` (an update rebuilds it).
- **Connection inventory no longer starts connections, and an ambiguous account is no longer
  guessed.** Both bundled skills told the agent to confirm a toolkit through
  `COMPOSIO_MANAGE_CONNECTIONS` with `action: "list"`. That call is not side-effect-free: on a
  toolkit with no connection on the selected identity it CREATES a pending authorization request
  and answers "All connections have been initiated and are pending completion"
  (`status: "initiated"`), so a plain "what is connected?" question left pending auth requests
  behind on both identities. The side-effect-free existence check is now the identity's own
  `COMPOSIO_SEARCH_TOOLS` — `toolkit_connection_statuses[].has_active_connection` plus the entry's
  `accounts[]` aliases — and `COMPOSIO_MANAGE_CONNECTIONS` is reserved for a toolkit already known
  to be connected there or for a connection the user asked for. In the same pass, the ambiguous
  identity rule became a MUST with its reason stated: when neither a pronoun nor a single
  connection settles the account, the first response is the question and never a tool call, because
  reading a calendar, inbox, chat or CRM on a guess exposes the requester's or a third party's
  private data to the whole conversation — a bare "check the calendar" had gone straight to a
  personal calendar read.
- **A stopped run stops answering.** "🛑 Stopped." is now the last thing the thread receives.
  The live answer message is created lazily by the first append that Slack accepts, so answer text
  still queued behind the rate limiter was created and posted by the stop path's own drain — the
  full reply landed underneath the stop card, unmarked, on both engines. Queued text is now dropped
  once a run is stopped, and the stop flag is re-read immediately before every delivery call (the
  streamed finalize and the chunked fallback) instead of being sampled once several awaits earlier.
  Text that had already reached Slack stays, closed with a `🛑 _Stopped — partial answer._` marker so a
  cut-off stream is never mistaken for a finished answer.
- **Codex Read mode works inside a container again.** Codex's default sandbox mechanism is
  bubblewrap, which cannot start under the container's `--cap-drop ALL` + no-new-privileges
  (`bwrap: Unexpected capabilities but not setuid`) and failed EVERY command — so a Read-mode
  channel could not even read. Runs that state a sandbox mode now also state
  `features.use_legacy_landlock=true`, the mechanism that does work under those caps: reads
  succeed, writes get "Permission denied". The admin bypass, which has no sandbox, is unchanged.
  The flag is deprecated-but-functional in the pinned CLI (`containers/versions.json`) and is
  re-checked on every Codex bump.
- **`/context` and reply footers report the real window for 1M models.** The `[1m]` suffix names a
  configured variant the CLI does not echo back (an `opus[1m]` run reports plain `claude-opus-5`),
  and the window was read off the runtime id alone — so a 1M run was measured against 200,000, five
  times too small, inflating every context percentage by the same factor. The suffix is now read
  from the configured model as well as the runtime one, and ahead of any runtime-reported window.
- **`/resume <command or id>` can adopt a session again.** Under the container runtime a thread's
  engine transcripts live in the channel's own HOME volume, but the lookup behind `/resume` only
  ever searched the daemon's own state dirs — so pasting back the very command `/resume` had just
  printed was answered with "I can't find … on the gateway machine", and no channel had ever
  adopted a session. The lookup is now runtime-aware: it walks the stores this channel can actually
  reach, cheapest first — the daemon's engine dirs (a legacy, pre-container session), the HOME
  volume read straight off the host where that is traversable, and otherwise the container itself
  through a new read-only runtime method, `inspectState` (one `sh -c` that globs the engine's
  layout and returns each match's mtime and capped opening lines; nothing is copied out, and the
  volume is never mounted). Rootless Podman is why the last step exists: it owns the volume's own
  directory as the mapped sub-uid with mode 0700, so the daemon cannot traverse into it even though
  the transcripts inside belong to its uid. The same-channel rule is unchanged and still decides on
  the cwd the transcript itself recorded. A refusal now also names the harness the PASTED command
  named rather than the thread's (a `claude --resume` line pasted into a Codex thread was reported
  as a missing "Codex session") and says where the gateway looked.
- **Schedules are stated in a named time zone, and containers run on the gateway's clock.** The
  daemon matches cron schedules against its own local time while a channel container was built on
  `Etc/UTC`, and nothing carried the daemon's zone across that boundary — so an agent asked what
  `15 9 * * 1-5` means read its own clock and answered "9:15 UTC" for a schedule that fires 09:15
  local. The container runtime now exports the daemon's IANA zone as `TZ` into every container and
  every exec (`process.env.TZ` first, otherwise the platform's resolved zone), so `date` and both
  engines see the same wall clock as the scheduler. `create_schedule` and `list_schedules` also
  NAME the zone rather than trusting any clock: a one-time schedule reports
  "2026-09-08 09:15 Europe/Bucharest (06:15 UTC)", a recurring one adds the resolved next fire
  time, and the bundled reminder guidance tells the agent to quote that zone back instead of
  converting it.
- **A reminder no longer stutters its own label.** A reminder whose text already began
  "Reminder: …" rendered as "⏰ *Reminder:* Reminder: review the QA results". One leading label is
  now stripped before the renderer adds its own — in the posted message, the unacknowledged 2nd
  notice and the creator's DM alike; a deliberate double, or a sentence that merely mentions the
  word, is left alone.
- **API runs report what a Codex run cost instead of "free".** `GET /api/runs/:id`, the
  `api_run_done` event and the completion webhook published `costUSD: null` whenever the engine
  reported no dollar amount — which Codex never does — while the usage ledger was independently
  storing a priced estimate for the very same run. The run now publishes the figure the ledger
  settled on (the canonical component rollup where a run reported one, so the API and the Audit
  view cannot quote two different costs), marked with a new `costEstimated` flag, and keeps `null`
  only when nothing anywhere knows. A stopped-mid-flight run is billed the same way.
- **A sentence before the first tool call no longer costs the turn its toolbox.** The live progress
  card is its own message posted above the answer, so once answer text starts a card that does not
  exist yet can only be created underneath it. The guard that protected that ordering latched the
  whole card OFF for the rest of the turn, and a model that wrote one preamble line before its
  first tool call therefore lost every tool, subagent and failed-tool row — the same run with
  tools first showed the full toolbox. Only the content-free liveness pulse is suppressed now: a
  tool, subagent, notice or progress-report row still opens (or keeps) the card at any point in the
  turn, while a text-only answer still stays a single message.
- **A progress stage no longer repeats its output two or three times.** Slack replaces a task row's
  title and status on every chunk but APPENDS its rich `details`/`output`, so a stage that carried
  its output when it was published, again when it flipped to complete, and once more in the
  snapshot that seals the card rendered the same paragraph three times over. Each row now remembers
  what it has already delivered to the current message and sends a rich field only when it actually
  changed — just the added tail when the value grew (an interrupted step's note), nothing at all
  when it is unchanged. A stream rollover reseeds its successor in full, since that message has
  rendered nothing yet.
- **A rate-limited stream finalization leaves one reply, not four.** When Slack rejected
  `chat.stopStream`, the classic recovery posted the complete answer as a new message while the
  partial streamed one stayed in the thread (truncated mid-sentence, no footer) and the run-stats
  footer arrived as yet another message — four bot replies for one turn. The recovery now deletes
  that partial copy first and attaches the footer and its controls to the answer it posts, so a
  failed finalization ends with the progress card and exactly one complete answer.
- **Fresh installs get their first-boot admin password again.** `npm run setup` answers the
  voice-transcription question before the daemon has ever booted and saves it through the settings
  writer, so a brand-new install already had a `settings.json` at first boot — and the first-boot
  check, keyed on that file's existence, treated it as a configured install and never minted the
  password. Every fresh install came up with `WARNING: no admin password` and the whole privileged
  API refused. The decision now keys on what the file holds: the installer's own keys
  (`whisperEnabled`) do not count as configuration; any operator-written key still does, so an
  existing install is never handed a password behind its operator's back. Found while installing a
  third gateway on the development host.
- **The daemon boots on the documented Node floor again.** Migration 17 created the channel-memory
  search index as an FTS5 virtual table; Node 22.13's bundled SQLite has no FTS5, so the migration
  threw and the whole database refused to open. The index is now optional per engine build
  (`src/db/fts.js` probes once per handle), the migration skips it with a warning, every open
  re-attempts the creation so a later Node upgrade gains the index without a new migration, and
  `search_channel_memory` answers from a plain scan (same AND semantics, diacritic folding and
  bracketed excerpts) until then.
- **CI's settings-generator coverage floor is met again.** The skills catalog reshaped the grant
  and listing paths in `src/gateway/folders.js`; `test/folders-generator-paths.test.js` now reaches
  work-folder containment, instruction updates, the host-folder grant fallback and the legacy
  managed-skill rename, and is part of the security-coverage gate.
- **Resumed Codex replies show the current message's cost, not the whole session's.** On a cold
  runtime probe the daemon learned Podman's host-side HOME-volume path only after run artifacts
  were materialized. Codex accounting then had no rollout baseline and treated its cumulative
  terminal counter as one reply. The settled read path is now refreshed immediately after the
  container starts, before the resumed turn snapshots usage; Claude's provider-reported per-turn
  cost and the canonical descendant-aware ledger are unchanged.
- **Composio (and every other injected remote MCP server) is back for Claude in channels that pick
  a global MCP server.** The channel lockdown lists a picked server by URL, and Claude Code then
  matches every REMOTE server by URL: a `serverName` entry no longer admits it, so `composio-user`,
  `composio-agent`, Skills Manager, Toolbox and the Make toolbox were dropped as "blocked by
  enterprise policy" — silently, before any connection attempt, which is why the run config still
  showed both Composio identities resolved while the model reported no Composio tools. Codex was
  never affected (it has no such allowlist). The lockdown now carries each injected remote server's
  URL beside its name (`injectedRemoteAllowMatches`, `src/gateway/mcp-catalog.js`); SDK-mode
  Composio admits its `*.composio.dev` tool-router hosts. Takes effect on the next turn after a
  restart — the per-run settings artifact is keyed by content.
- **A Claude thread inside a Codex-default channel no longer starts "Not logged in".** The
  relayed Claude login was resolved for the CHANNEL's harness before the thread's own harness was
  settled, so a thread that had started on Claude while its channel later moved to Codex spawned
  Claude with no `CLAUDE_CODE_OAUTH_TOKEN` — inside a container that is Claude Code's own
  "Not logged in · Please run /login", two seconds in, every time (`src/gateway/run.js`). The
  credential is now resolved after the thread-engine decision; the stub engine reports whether it
  received a login so the E2E can prove the spawn was authenticated.
- **The nightly engine canary installs Claude Code the way the product documents.** Its Claude
  jobs had failed on both runners since the npm package moved to a native binary fetched by its
  postinstall — `npm install --ignore-scripts` left a shim with nothing to run ("executable format
  is invalid" on macOS). The pinned and the drift-probe jobs now use the official installer
  (`2.1.258` / `latest`), and the Codex target moves to `0.152.0`, the first release whose network
  proxy keeps approved tunnels on Linux (`.github/workflows/nightly-canary.yml`,
  `docs/COMPATIBILITY.md`).
- **CI is green on macOS again, and the security-coverage gate no longer depends on the runner's
  toolchain.** Thirteen tests failed only on macOS because `os.tmpdir()` there is a symlink
  (`/var/folders/…` → `/private/var/…`) and the code under test resolves REAL paths — credential
  files, toolchain binaries, custom work dirs — so a fixture built from the symlinked form never
  string-matched what the code reported. The shared test scratch root is now canonical
  (`test/helpers.js`); Linux reproduces the old failure with a symlinked `TMPDIR`. The access-grants
  coverage area gained tests for the stable toolchain launcher dir (with an explicit fixture
  toolchain instead of whatever `~/.local/node` the runner happens to have), the isolated-target
  refusal, an unreadable `.claude/agents` directory, every post-signature refusal of a gateway
  capability, and `userOnlySkillGrants`.
- **`/update` rebuilds the channel image when this revision needs a new one.** The updater pulled,
  installed dependencies and restarted; the container image was built only by a manual
  `npm run build:image`. So an update that changed `containers/` or bumped `imageSpecVersion` left
  every container channel running the previous toolchain until an operator noticed the boot warning.
  With the container runtime on, the update now runs the build itself — after dependencies, before
  the restart — whenever the candidate touched `containers/`, moved the spec version, or no image is
  built at all. The decision is a pure `needsImageBuild()` in `src/runtimes/container/image.js`
  (unit-tested), the built version comes from the image's own `cg.image.version` label, and the
  expected version is read out of the CANDIDATE's `containers/versions.json` rather than a constant
  the runner imported before the checkout moved under it. A failed build never blocks the update:
  it reports `channel image build failed — run \`npm run build:image\`` and continues to the
  restart, because the image already on disk still runs every container channel.
- **The approval card no longer calls a container channel's job "unsandboxed".** The card for a
  background shell job in an AUTO channel told the admin it "Runs OUTSIDE the engine sandbox as the
  daemon user" — true on the host, and false in a container channel, where the job runs inside that
  channel's own container on the image's toolchain with only the channel's mounts. The card now
  reads the run's target and says `Runs inside this channel's container (<image>)` for an isolated
  runtime, keeping today's wording for the host. Driven by `runtimeSupports(target, "isolated")`,
  never a backend id, and it is still an admin-tier click either way.
- **The test suite cleans up after itself.** Every test process created a scratch gateway root and a
  sibling TMPDIR and removed neither — roughly three hundred directories per `npm test`. On a host
  whose `/tmp` is a tmpfs with a fixed inode budget, the accumulation eventually exhausted the
  inodes and unrelated work started failing with "unable to open database file". `test/helpers.js`
  now tracks every scratch directory it hands out and removes them on process exit (`node --test`
  gives each file its own process, so that is complete), every `mkdtempSync` in the suite goes
  through that helper, and a new `pretest` step (`scripts/test-scratch-sweep.mjs`) sweeps anything
  older than two hours that an earlier crash left behind, skipping what it cannot remove.
- **Claude now runs on the operator's own login, host and container.** The gateway used to symlink
  `~/.claude/.credentials.json` into its synthetic engine home. Claude Code writes that file by
  RENAME, so the first refresh a gateway run performed replaced the link with an independent copy of
  the operator's session — which then aged out on its own while the operator's real login stayed
  valid, and every Claude turn on the affected gateway silently failed over to Codex. Nothing plants
  that link any more. `src/gateway/claude-login.js` is one resolver for "which Claude login does the
  gateway use" — a configured `claude setup-token`, else the host user's own `~/.claude`, else a
  login signed in to the engine home, else `ANTHROPIC_API_KEY`, else a named remedy — and the relay
  reads and refreshes THAT source, in that login's own config dir. Every run now receives its current
  ACCESS token in `CLAUDE_CODE_OAUTH_TOKEN`, host runs included (previously containers only), so a
  host child never depends on a credentials file the gateway no longer maintains. Fail-closed stays
  container-only; a host turn with no login logs the remedy once an hour and lets the engine speak.
  Boot, `/api/health` and `/status` name the login source and the date its session dies, and warn
  three days ahead. No setup-token is required for anything any more.

### Removed
- **Settings → Network → "CLI integrations".** The switch, its `cliIntegrations` setting, the
  live "installed" badges and the read-only linking of the daemon's shared host login
  (`~/.supabase`, `~/.vercel`) into runs are gone. ChannelGate now targets Linux with the container
  runtime only: a container has no domain allow-list and its image ships `vercel` and `supabase`,
  so the switch had no effect there, and a channel's own provider login is a `/secrets` variable —
  never the daemon's host-wide file, which is the identity mix-up the switch invited. `/secrets`
  now suggests every catalog name; a stored `cliIntegrations` value is inert. First slice of
  retiring the host sandbox runtime and macOS.
- **The host sandbox runtime and the network allow-list.** Claude Code `sandbox.*` settings
  generation, the Codex host permission profiles and `network_proxy` compilation, the toolchain
  launcher grants, the host credential links, the `SENSITIVE_HOME` deny lists, the AppArmor/userns
  fix, the per-domain egress allow-list (`networkDomains`, per-channel `extraNetworkDomains`, the
  `request_network_domain` tool and its card), the container kill switch and the per-channel
  runtime pin are gone. The container is the boundary: every turn — admin channels included — runs
  in the channel's own rootless Podman container with a per-channel HOME volume and only the work
  folder, clean workspace and artifact dir mounted, and the daemon refuses to boot without a
  container CLI. Modes stay as tool-permission presets; `.claude/settings.json` keeps permissions,
  the MCP allowlist, memory-off and the Stop hook, with no `sandbox` block. *Allow network* stays as
  a per-channel switch the engines are told about — it filters nothing and does not yet cut a
  container's egress (every container is on the bridge network); the egress proxy is a later slice.
- **macOS support.** ChannelGate runs on Linux only — systemd for the service, rootless Podman for
  every channel. Gone: the launchd installers (`scripts/install-launchd.sh`,
  `scripts/uninstall-launchd.sh`, the `service:*:boot` npm scripts), the darwin branches of the
  self-updater (launchd probe, `kickstart` restart, `bootout` + `bootstrap` reload), the
  migration's plist rewrite, the admin Stop route's `launchctl bootout`, the shutdown exit-code
  rule for `KeepAlive`, the Homebrew/CMake Whisper source build, the extra macOS disk requirement,
  and the macOS legs of the CI and nightly matrices. `npm run service:install` /
  `service:uninstall` now wrap `scripts/install-systemd.sh` and the new
  `scripts/uninstall-systemd.sh` (system AND user units, current and pre-rename names, never the
  runtime root); `npm run setup` and `src/start.js` refuse any other platform with one plain line
  ("ChannelGate runs on Linux only (systemd + rootless Podman); this host is <platform>."), and the
  documentation describes one platform.

### Changed
- **The repository is `makeitfutureDev/channelgate`.** Development, the served `main` and the
  landing lock (`refs/channelgate/landing-lock`, renamed from `refs/claude-gateway/…`) live in the
  new repository, whose history starts from one fresh-start commit of the scrubbed tree; the
  previous repository is a read-only archive of the full history. An existing checkout is repointed
  with the three commands in `INSTALL.md` → *Upgrading* — a plain `git pull` cannot fast-forward
  across unrelated histories, and the updater refuses a diverged checkout by design.

### Added
- **Failover after the in-place retries, with a choice of who decides.** A provider that stays
  unavailable through the transient retries now fails over to the other harness (five-minute
  cooldown for the channel), and Settings gained *How a failover happens*: `auto` (the silent switch)
  or `ask` — a card in the Slack thread with *Switch to <other>* / *Try <failed> again* buttons; the
  click re-runs the original message on the chosen harness (*Switch* pins the thread there). When
  both harnesses fail the error names both in one sentence and a watched thread gets the same card.
  The retry pause hands the run slot and container lease back for its length. The same-engine
  gateway-default-model retry now covers Claude (`model_not_found`) as well as Codex
  (`src/gateway/run.js`, `src/slack/engine-switch-choice.js`, `src/engines/stream.js`).
- **Transient provider failures are retried in place.** A 5xx, an "overloaded", a connection
  reset/timeout, or an unexplained 404 from the Codex backend (the 2026-09-03 ChatGPT Codex outage
  failed every turn on both gateways with "404 Not Found: Unknown error" for a few minutes) no
  longer ends the turn with a red error on the first try: the gateway re-runs the same turn on the
  same engine up to two more times, ten seconds apart (`CG_TRANSIENT_RETRY_ATTEMPTS`,
  `CG_TRANSIENT_RETRY_DELAY_MS`), only while no tool has run, and never for authentication,
  usage-limit or model-rejection failures, which keep their own failover / model-retry paths. Every
  attempt is a `run_transient_retry` event and a status-line notice ("retrying in 10s (1/2)"); a
  reply that needed more than one attempt says so in one line, an exhausted error says how often it
  was retried, and a cancel ends the pause early (`src/gateway/run.js`, `src/engines/codex.js`).
  Review fixes on the same change: a retried FRESH Claude session runs under a new session id (the
  CLI refuses to create one twice — "Session ID … is already in use"); the warm Claude process, which
  stays alive after a provider failure, now rejects that turn with the classified error instead of
  posting "API Error: …" as the reply (`src/engines/persistent-session.js`); a turn that already
  streamed text is never replayed (Claude's replay-safety now matches Codex's); Claude's catch-all
  `provider` kind and the bare "API Error:" prefix no longer count as an outage (a rejected model or
  an unknown 4xx fails once); Codex's stderr never decides a replay (a wedge or an unexplained exit
  keeps its honest message, and a recovered "unexpected status 429" line no longer becomes a
  usage-limit cooldown), the underscore error codes it actually emits (`internal_server_error`,
  `response_stream_disconnected`, …) do qualify, a 404 whose body names the model is a model
  rejection, and the stderr excerpt in a classified failure is redacted; the retry knobs are read
  per turn (so `.env` values count) and the kinds each engine may replay are an adapter fact
  (`transientKinds`). Known cost, inherent to a replay: on a RESUMED session the failed attempt's
  prompt (and the provider's error line) stay in the transcript before the retried one.
- **`--repath --from <old> --to <new>` for a folder moved by hand.** The rename migration's rules
  only know the roots the product renamed; a folder the operator moved — the daemon's own checkout,
  a channel's custom `workDir` — was invisible to them, so its channel record, Claude project
  directory and transcripts, engine `.claude.json`, Codex `config.toml`, memory prose and service
  definition kept naming the old place, and thread resume in that channel broke. The pair
  (repeatable) is now one more rewrite rule for every pass, `--verify` handed the same pair counts
  what is still stale, and a malformed or misplaced pair is refused with usage before anything is
  read (`scripts/migrate-channelgate.mjs`, `docs/OPERATIONS.md`).
- **The Claude login expiry now reaches an admin in Slack, without a restart.** The three-day
  warning was evaluated only at boot and in `/status`, so a daemon that had been up for weeks never
  raised it — the first symptom was Claude turns silently failing over to Codex. A new hourly watch
  (`src/gateway/login-watch.js`, started next to the nudge sweep and stopped with the other runtime
  services) re-resolves the login and DMs every admin once per UTC day: from three days before the
  session expires, and daily while there is no usable login at all. The DM names the login kind and
  config dir, the expiry in UTC and in the gateway's local timezone, and the remedy — never any
  token material. The "already told them today" marker lives in the `_meta` table, so a restart
  cannot re-spam; a class change (expiring → missing) still notifies the same day, and a login that
  goes healthy clears the class. One unreachable admin does not stop the others, and a tick that
  reached nobody stays due for the next hour.
- **Container Claude runs accept the daemon's API key.** A Linux service install authenticates
  with `ANTHROPIC_API_KEY` (docs/OPERATIONS.md) and has no login to relay; container turns used to
  fail closed with "no Claude login to relay" on exactly that install. The key is now the third
  credential mode (setup-token → relay of a login → API key), gated from the live environment.
- **Image spec 1.1.1 — the image PATH now reaches Codex's login shells.** Codex runs commands
  through `bash -lc`, and Debian's `/etc/profile` reset PATH to the distro default, so a Codex
  shell in a container never saw `~/.npm-global/bin` (npm -g installs) or `/opt/channelgate/bin`.
  `/etc/profile.d/channelgate-path.sh` re-asserts the image PATH after that reset; a durability
  test pins it. Also fixes the opt-in live durability test's marker parsing.
- **A channel can run its engines inside its own long-lived Linux container.** Roughly a hundred
  commits of sandbox repair were all fights with a *subtractive* boundary — start from the whole
  host, enumerate the denies. A container is *additive*: the channel gets only what the image and
  the mounts provide, and the whole bug class becomes inexpressible. v0.8 P1 lands the runtime
  seam and the container backend; it **deletes nothing** — the `host` backend stays fully working,
  admin channels stay on it, and a gateway-wide kill switch returns every channel to it at once.
  - **`src/runtimes/` — a runtime backend registry** (`contract.js`, `registry.js`, `resolve.js`,
    `host.js`, `container/`), to WHERE an engine runs what `src/engines/` is to WHICH engine runs.
    A backend that omits a method or a declared capability does not load; an unknown capability key
    throws. `resolveRuntime()` decides once per turn and hands the target to the folder generator,
    the MCP builder, the runners, the artifact paths and the session stamp — nothing downstream
    compares a backend id to a literal.
  - **Precedence**: `containerRuntimeEnabled:false` → host · admin mode → host · `meta.runtime` →
    that · else `containerDefaultBackend`. A per-run override can only reduce capability, so it can
    never move a turn between backends.
  - **The container**: rootless Podman preferred (`auto` = podman, then docker), `--userns=keep-id`
    (or a pinned `--user`), identical-path bind mounts of the channel workdir, the clean workspace
    and a per-channel artifact dir under `~/ChannelGate/.runtime/`, a per-channel HOME **volume** at
    `/home/agent`, the control socket read-only at `/run/channelgate`, a `/run` tmpfs (with `/tmp`
    and `/var/tmp` persistent — see the durability entry below), `--cap-drop ALL`,
    `no-new-privileges`, and pids/memory/cpu caps that degrade with a warning where cgroups are not
    delegated. The gateway root, `config/`, `gateway.db`, the channel metadata folder, the daemon
    checkout and the operator's `~/.claude`/`~/.codex` are never mounted. Run environment travels in
    a 0600 `--env-file`, never on argv.
  - **Lifecycle**: created on first use, restarted in under a second when exited, recreated only
    when the create-time fingerprint changes (and only when no lease is held), stopped — not
    removed — after `containerIdleMinutes` (default 10) with runs, background jobs and memory-review
    runs each holding a lease, and bounded by `containerMaxRunning` with LRU stop and an honest wait
    rather than killing someone's job. A boot reconcile sweeps the previous daemon's foreground and
    warm process groups and leaves detached background jobs running.
  - **Engine logins**: Claude prefers a `claude setup-token` value injected as
    `CLAUDE_CODE_OAUTH_TOKEN` (no mount, no shared refresh chain) and otherwise seeds one copy of
    the gateway's credentials into the channel's HOME; Codex gets a shared read-write file mount of
    the real `auth.json` because it refreshes in place, with a per-channel `CODEX_HOME` so sessions
    and history stay per channel. No login at all fails the turn closed with the exact remedy
    instead of failing over to an engine that would also fail.
  - **Inside the container each engine's own sandbox is off** — the OS boundary the daemon owns is
    the confinement. `permissions.allow`, the mode mapping, the memory-off flags, the MCP allowlist
    and the admin-only bypass are unchanged, and the Stop hook and MCP helpers come from the image's
    `/opt/channelgate` bundle rather than this checkout.
  - **The gateway control MCP is served by the daemon over a unix socket** (`run/mcp.sock`, 0600 in
    a 0700 directory, mounted read-only) instead of a stdio child that would need the database,
    the config directory and the daemon port mounted into every channel. One hello line, then MCP;
    identity is the signed run capability alone, with `toolset` and `progressReport` as SIGNED
    claims; `CG_APPROVAL_SECRET` and `CG_PORT` never enter a container. Minting and verification now
    happen in one process, retiring the aud/secret-skew class permanently. Composio SDK mode rides
    the same socket as a second service.
  - **The stall watchdog learned a third answer.** A container child's pid names the exec client,
    not the engine, so liveness is an async backend probe of the run's process group — and a probe
    that throws, times out or is inconclusive reports *unknown* and the turn keeps waiting. Only a
    definite "gone" ends a turn; a quiet turn is still reported, never killed.
  - **Background jobs and memory review** resolve their own backend at their own spawn, hold a
    lease, and run detached inside the container; job output is tailed from a log at an identical
    path on both sides and the real exit code comes back through a `[cg-exit:N]` marker. A recovered
    job is probed and signalled by its run id, never by a stale client pid.
  - **Where a run happened is recorded and shown**: an additive `sessions.runtime` column
    (migration 13, `''` default, so older code still boots on the new schema), a `/status` Runtime
    line, a ` · container` heartbeat suffix, the image ref on the reply footer, and a `/resume`
    command in the container's own form (`podman exec -it -w <cwd> <name> …`).
  - **Admin surface**: a Settings → Container runtime card (kill switch, default backend, CLI,
    image, idle minutes, max running, pids/memory/cpu caps, and the write-only Claude token), a
    per-channel/DM **Runtime** select that shows the effective decision beside the stored pin, the
    `set_channel_runtime` chat tool, and `containerRuntime` in `/api/health`. Every value that would
    reach the container CLI's argv is refused with a 400 rather than sanitized.
  - **`npm run build:image`** builds `containers/Containerfile` with the daemon user's uid/gid baked
    in, the CLI versions pinned in `containers/versions.json`, and an in-image bundle resolved as
    the import closure of the three helpers an engine spawns — the checkout is never mounted and
    never copied. Because a tag is a moving pointer, the container fingerprint uses the resolved
    image ID, so a rebuild retires containers on their next run.
  - **Cross-platform**: a host with no container CLI boots exactly as before and reports why in
    `/api/health`; the channel image and its helper scripts are Linux-only by design, and macOS
    stays a supported daemon and development host on the `host` backend.
- **A thread's engine history follows it when its channel changes runtime backend.** Stop, start and
  recreate already lost nothing (the HOME volume and the workdir bind outlive a container). The one
  remaining loss was a thread whose channel MOVED — host → container when it is containerized,
  container → host when it is set to admin mode, pinned back, or caught by the kill switch: the
  engine's state dir moved with it, the resume found no session, and the turn was healed instead,
  which keeps the conversation readable but throws away the compactions, tool results and subagent
  transcripts the chat transcript never held. The session's files are now carried across before the
  first resume attempt — lazily, per thread, in both directions, overwriting the older copy and
  deleting nothing on either side.
  - **`sessionState`, a new per-engine fact** (`src/engines/adapters.js`): where a harness keeps a
    session, as paths relative to its state dir. Claude: `projects/<cwd-key>/<id>.jsonl` plus the
    `<id>/` subagent directory, keyed off the RUN's cwd so clean mode keys the directory it ran in.
    Codex: `sessions/YYYY/MM/DD/rollout-*-<id>.jsonl` — a pattern, because the filename carries a
    timestamp nobody can recompute, and the date tree survives the copy because that is what
    `codex exec resume` walks. An engine that declares none is skipped, never guessed at.
  - **`copyIn`/`copyOut`, two optional runtime-backend methods** (`src/runtimes/contract.js`): host
    is a plain `node:fs` copy; the container stages through the bind-mounted artifact dir and runs
    ONE `sh -c` inside, because the daemon cannot reach a HOME volume. A staged file naming a path
    outside the requested state dirs is refused rather than written. Optional is still fail-closed:
    a backend that hangs a non-function on one of these names does not load.
  - The session row is **re-stamped the moment a carry succeeds**, so it always names the side
    holding the newest copy — without it the next turn would carry the stale copy back over
    everything this one added. `run_config` records `sessionCarried` when it happened, and every
    failure is one log line plus the heal that was already there.
  - The idle reaper now starts even with the gateway kill switch OFF: flipping that switch is the
    container → host transition, and a carry may bring one container up to read its HOME volume, so
    something has to stop it again. It only ever acts on containers this process started.
- **Nothing a container channel accumulates is lost any more — including `/tmp` (image spec 1.1.0).**
  A channel container is stopped as a matter of routine (the ten-minute idle sweep, the
  max-running cap) and recreated whenever its create-time fingerprint changes, so durability is a
  question about an ordinary Tuesday, not about a rollback. The HOME volume and the workdir bind
  already survived all of it; `/tmp` and `/var/tmp` did not, because they were tmpfs — every idle
  stop silently emptied them, a regression against the host backend where Claude Code's
  `/tmp/claude-<uid>/…` scratchpad survives between turns.
  - `/tmp` and `/var/tmp` are now rw bind mounts of `~/ChannelGate/.runtime/<platform>/<slug>/tmp`
    and `…/var-tmp` — per channel, never shared, never deleted by the daemon, and visible on the
    host so an operator can see what an agent parked there. `/run` stays the only tmpfs (pid files
    and the read-only socket mount, which must be fresh at every start). The tmpfs size caps are
    gone with the tmpfs: both trees grow against the disk, exactly like the work directory. The
    mount list is part of the container fingerprint, so every existing container is recreated once
    on its next run — with its HOME volume, which is the proof that a recreate keeps everything.
  - **Image spec 1.1.0** widens the PATH to every place a channel can install into, ahead of the
    pinned toolchain: `~/.npm-global/bin`, `~/.local/bin`, `~/bin`, `/opt/channelgate/bin`, the
    distro dirs, then `~/.cargo/bin`, `~/.bun/bin`, `~/.deno/bin`, `~/go/bin`. All of them are
    inside the per-channel HOME volume, so an installed CLI stays installed across a stop, a restart
    and a recreate. It also adds `python3-pip`, `python3-venv` and `pipx` with `PIP_USER=1` +
    `PIP_BREAK_SYSTEM_PACKAGES=1` and `PIPX_BIN_DIR=~/.local/bin`, so a plain `pip install <cli>`
    lands in the volume instead of failing on Debian's externally-managed-environment marker.
    `apt`/`sudo` remain deliberately unavailable — the toolchain is root-owned so a channel can
    never replace its own engines. Rebuild with `npm run build:image`; the daemon compares the built
    image's spec label against the one the checkout expects and names that command at boot when they
    differ.
  - Pinned by `test/container-durability.test.js` (the mount and tmpfs contract, the idle sweep and
    the boot reconcile issuing no destructive verb, a recreate reusing the same HOME volume, a
    source scan proving no production caller passes `destroy(…, { volumes: true })`, and the
    Containerfile/PATH facts) plus an opt-in live proof against a real podman,
    `npm run test:live-container`.

## [Unreleased] — One /model wizard replaces /engine + /effort (2026-07-16)

### Added
- **Google Chat and Microsoft Teams transports (preview).** Both surfaces can now receive and
  answer messages. Google Chat runs outbound-only on a Cloud Pub/Sub PULL subscription (no inbound
  endpoint, no tunnel — the same posture as Slack's Socket Mode); Teams runs on the documented Bot
  Framework endpoint `/api/teams/messages`, which authenticates every request against the Bot
  Framework JWKS before anything else happens. Credentials, live connect/disconnect, and per-platform
  health are in the admin Settings page; setup for both is in `docs/PLATFORMS.md`. A turn on either
  surface goes through the same gate, authorization, confinement, and usage accounting as a Slack
  turn — and neither is escalatable to full access, because escalation needs an interactive
  permission prompt neither surface has yet. Written against `node:crypto` and `fetch`: no new
  dependency for either platform.
- **License keys, tiers, and usage limits (`src/ee/`).** A deployment without a key serves one
  conversation per UTC month with 500 AI messages in it; a free key unlocks every conversation and
  keeps the 500-message monthly cap per conversation; an enterprise key removes both. Limits are
  server-defined and ride a signed payload, so the Licensor can change what a key is worth without
  a release. `src/ee/` is **proprietary, source-visible** code owned by MAKEITFUTURE S.R.L. and is
  not under the Sustainable Use License — see `src/ee/LICENSE-EE.md` and `LICENSE.md` §3.2/§4.5.
  - **Verification** (`src/ee/license.js`): the key comes from Settings (listings return
    `hasLicenseKey` + the last four characters only; the value is revealable one at a time through
    `POST /api/secrets/reveal`) or from `CHANNELGATE_LICENSE_KEY` as a bootstrap. The daemon checks
    `POST {platform}/v1/license/verify` at boot and every 24 h, verifies the response's Ed25519
    signature locally, and caches it. Verification is **never awaited on the boot path** — Slack
    connects while it is in flight, and the run gate reads the cached state.
  - **States**: `no_key · valid · invalid · revoked · grace · expired_grace`. Unreachable keeps the
    last verified tier for 14 days; past that it is still kept until the **next UTC month
    boundary** and only then falls back to the no-key limits — never mid-month, never silently.
    `invalid`/`revoked` drop immediately. Every non-quiet state raises a banner in the admin UI.
    A response whose signature does not verify changes nothing in either direction.
  - **Enforcement** (`src/ee/limits.js`, called from `licenseAdmission()` in
    `src/gateway/run.js`): conversation admission (the first N distinct conversations of the UTC
    month are the allowed set, persisted in the new `license_usage` table — schema migration 12)
    and the per-conversation monthly cap, counted **at spawn** for every origin except the
    deployment's own memory-review runs, with a one-time 80 % warning. A refused turn is a short,
    platform-degraded reply carrying the sign-up link — never an error, never silence, and no run
    starts. Nothing here can crash or kill a run.
  - **Admin UI**: a License card under Settings — state banner, tier, key last four, last/next
    verification, expiry, installation id, *Verify now*, key set/clear (with the existing reveal),
    the platform URL, and this month's per-conversation usage bars against the limit line.
  - **Gateway MCP tools**: `get_license_status` (any allowed user, never shows the key) and
    `set_license_key` / `clear_license_key` (admins only, behind the control-plane approval click).
  - **Data leaving the install** is exactly two payloads — the verify request (key, installation
    id, version) and the daily/at-shutdown usage report (installation id, key hash, version, UTC
    month, per-conversation **hashes** and counts). Never message content, user ids, channel names,
    or credentials. Documented in `docs/PRIVACY-AND-DATA-FLOW.md` and `docs/LICENSE-KEYS.md`.
  - **Air-gapped deployments** can run on `CHANNELGATE_LICENSE_PAYLOAD`, a signed license verified
    locally against the same public key; such an install makes no outbound request at all.
  - The Ed25519 verification key shipped in `src/ee/license-public-key.js` is a clearly labelled
    **placeholder**; the production key is swapped in at deploy time, and
    `CHANNELGATE_LICENSE_PUBLIC_KEY` overrides it for staging and tests.

### Changed
- **Admin Settings loads with the multi-platform registry enabled.** Platform UI manifests now
  contain data only: runtime adapter functions (including optional helpers such as `normalizeName`)
  are removed generically before cloning, preventing `/api/settings` from failing with a
  `DataCloneError` when Google Chat and Teams are registered.
- **README rewritten as a hero landing page, plus GitHub repository metadata**: above the fold the
  README now opens with the H1, the tagline the site's meta description repeats verbatim, a badge
  row (Sustainable Use License 1.2, CI, Node ≥ 22.13, the three chat platforms, the three engines),
  a three-sentence what-it-is, a placeholder for a 45-second demo GIF, a seven-step "Running in 10
  minutes" quick start, a ten-row feature grid, the architecture flow, a ✅/➖/❌ comparison table
  footnoted as a generalisation about categories, security in five bullets, the Free / Partner /
  Reseller-white-label-enterprise licensing lanes, one UTM-tagged Makeitfuture CTA, and a
  documentation index. Every operational section (Slack app setup, install, updating, admin UI,
  engines, configuration, security model, environment variables, admin password) is preserved
  below it. New: `.github/REPO-METADATA.md` (About text, website, the ten repository topics and the
  `gh repo edit` command to apply them at launch) and `docs/assets/README.md` (specs for the demo
  GIF and the 1280 × 640 social preview — neither binary is committed). `test/readme.test.js`
  guards the shape: one H1, one "formerly" attribution, never "open source", the CTA and UTM links,
  every relative link resolving, well-formed badge URLs, and the licensing facts.
- **Renamed to ChannelGate, with new folders and a boot migration** (formerly
  *Claude Gateway for Slack*): the display name, the npm package (`channelgate`), the Slack app
  manifest, the launchd label (`com.makeitfuture.channelgate`), the systemd unit
  (`channelgate.service`), and the bundled lockdown skill (`.claude/skills/channelgate`) all
  follow the product name. The hidden runtime root moves from `~/.claude-gateway/` to
  `~/.channelgate/` (env `CHANNELGATE_DIR` / `CHANNELGATE_DB`, with `CLAUDE_GATEWAY_DIR` /
  `CLAUDE_GATEWAY_DB` still honoured for one major behind a one-time deprecation warning), and
  the visible workspace root moves from `~/Slack Agent/<slug>/` to
  `~/ChannelGate/<platform>/<slug>/` — `slack/`, `teams/`, `google-chat/`, taken from the
  platform registry so a new surface adds a folder by adding an adapter. Per-channel metadata
  (`<root>/channels/<platform>/<slug>/`) and clean-mode workspaces move the same way; a custom
  per-channel `workDir` is never touched. `scripts/migrate-channelgate.mjs` runs once at boot
  (before the database opens and before Slack connects) and on the post-update step: it refuses
  while the old daemon holds its singleton/update lock, prints a dry-run plan, moves the runtime
  root, moves each channel's folders by its stored platform, rewrites stored absolute paths,
  regenerates every channel's `.claude/settings.json`, drops warm sessions, and leaves a
  `MOVED.md` breadcrumb. A failure never fails the boot — the daemon continues on the old paths.
  `node scripts/migrate-channelgate.mjs --dry-run` prints the plan without changing anything.
- Repository restructure for the public release: product docs merged into `docs/WHY.md`,
  engine capabilities moved to `docs/`, marketing site moved to its own repository,
  internal names scrubbed.

### Licensing
- **Makeitfuture Sustainable Use License 1.2 (2026-08-25)**: the Software is renamed ChannelGate;
  new §3.2 adds license keys and usage limits (no key: one conversation; free key: unlimited
  conversations at 500 AI messages/conversation/month; enterprise: unlimited) with end-user keys
  only, and §4.5 makes circumventing them a violation; §3.1 states that any number of separate
  single-customer deployments are permitted service work on the customer's key; §4 names the
  Reseller, White-Label, Enterprise, and optional Partner agreements; the former §7 Change Date is
  removed — no automatic relicensing; new §11 sets Romanian law and Bucharest courts. New
  `docs/LICENSE-KEYS.md`, `TRADEMARK.md`, `AUTHORS.md`; `CLA.md` 1.1; FAQ and decision record
  amended. Docs only — the key enforcement itself is the `src/ee/` slice.
- **Makeitfuture Sustainable Use License 1.1 (2026-08-20)**: new §3.1 permits operating a
  dedicated deployment for a single customer as paid service work and states its four conditions;
  §4.2/§4.4 make multi-tenant operation and white-labeling explicit restrictions; new §6 plus
  `CLA.md` set inbound contribution terms with a `Signed-off-by` sign-off; new §7 adds an
  irrevocable Apache-2.0 Change Date four years after each version's publication. Worked examples
  in `docs/LICENSING-FAQ.md`; rationale and the enterprise-tier boundary policy in
  `docs/LICENSING-DECISION.md`. No code or feature changes.

### Changed
- **Community and contribution files added**: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`
  (Contributor Covenant 2.1), `SUPPORT.md`, `.github/CODEOWNERS`, issue forms (bug, feature,
  partner inquiry) with blank issues disabled, a pull-request template, and a dependency-free
  `scripts/check-dco.mjs` (`npm run check:dco`) that fails any commit without a `Signed-off-by`
  trailer — enforced in CI on every pull request.
- **File-explorer uploads are browser-only**: the native Slack `file_input` modal and private Slack
  file download path are removed. One *Upload files / folder* button sends multi-file and folder
  selections directly into the confined gateway directory, so the ingest never uses Slack storage.
- **`/model` is now the single runtime command**, a four-step wizard in one self-updating message:
  scope (*This channel* or *Just this thread*, buttons) → harness (*Claude* / *Codex* buttons, plus
  *Use defaults* to clear the scope's overrides) → model dropdown → effort dropdown. Every step
  persists the moment it's clicked. Admin-only in channels, open to approved users in DMs. Typed
  `@bot /model` is the command — no Slack-manifest slash command is registered (a Bolt handler
  answers anyway, thread-aware, if one is ever added to the app).
- **Reply footers show the governing model**: the model in the run-stats footer follows the
  configured cascade — thread override → channel/DM model → gateway default — and only when
  nothing is configured anywhere, the CLI-reported model (the engine's own default). Context-window
  % and Codex cost estimates still key on the CLI-reported runtime model.
- **Admin-UI model fields are dropdowns** (were free-text "e.g. opus" inputs): Settings' two
  per-engine defaults plus the channel Runtime card and channel/DM config editors all pick from
  the same curated options as the `/model` wizard, following the selected/inherited engine. A
  hand-edited non-curated id survives as an extra option (Save round-trips it); switching engine
  drops the other engine's pick.
- **`/engine` and `/effort` are retired** — typed or slash-command invocations answer with a pointer
  to `/model`, and old `/engine` dropdown messages in history reply with the pointer instead of erroring.

### Added
- **Create files and edit general UTF-8 text from `/files`**: writable views now show *New file*
  beside the existing upload/folder controls. The Slack modal creates one confined file with
  optional initial text using exclusive, non-overwriting semantics and records
  `channel_file_created`. Editing is content-based instead of limited to `.txt`/`.md`, so `.env*`,
  JSON/YAML/TOML, configs, scripts, and extensionless UTF-8 files use the existing Worker/Auto
  permission boundary; Full remains admin-only. Managed, credential/token/secret, key, binary,
  invalid-UTF-8, oversized, traversal, and conflicting paths remain protected.
- **Multi-file and folder upload**: the browser-only *Upload files / folder* flow preserves
  directory structure for up to 200 files / 250 MB total using a one-time user/channel/folder grant,
  HttpOnly SameSite session,
  CSRF, repeated authorization/membership/mode checks, per-file confinement and 25 MB limits,
  collision-safe writes, and `channel_file_uploaded_in_browser` audits. Empty directories are not
  represented by browser folder pickers.
- **Create folders from the native file explorer**: writable channel modes now show *New folder*
  beside *Upload files / folder*. The modal creates one directory inside the folder currently on screen,
  repeats authorization/membership/mode checks, rejects protected names, separators, traversal, and
  collisions, and records `channel_folder_created`. Read-only hides the control; Full remains
  admin-only.
- **Native channel file explorer**: `/files` opens a Block Kit modal over the current channel's
  effective working folder; a message shortcut and typed `@bot /files` cover thread-scoped use.
  Navigation is AI-free, authorization/membership is rechecked, realpaths and symlinks stay confined,
  gateway internals and likely secrets are hidden, previews are bounded, and a confirmed selected
  file (≤25 MB) is uploaded only to the originating channel/thread with an audit event. Preview now
  states that truncation is display-only; *Send to me* delivers the complete file privately through
  the bot's Slack DM. Worker/Auto modes now open eligible UTF-8 `.txt`/`.md` files in a full browser
  editor (250,000 characters / 1 MB) through the configured public gateway URL, including a live
  Markdown split preview. One-use file grants exchange for HttpOnly editor sessions, with CSP,
  no-store, CSRF, revalidated authorization/membership/mode, an open-time hash conflict check,
  atomic replace, and audit logging; Full mode remains admin-only. The 3,000-character Slack editor
  stays as the no-public-URL fallback.
- **Per-thread model + effort overrides** (`thread-models.json` / `thread-efforts.json`, next to the
  existing per-thread engine override): the wizard's thread scope pins harness/model/effort for one
  thread only. Run-time precedence: per-run API override → thread → channel/DM → gateway default.
  A harness switch (wizard or `claude`/`codex` directive) drops thread model/effort that don't
  belong to the new engine.
- **Engine-switch context replay is now detection-based**: the thread's session row remembers which
  harness minted it, so ANY switch (wizard thread/channel scope, admin UI, flipped gateway default —
  not just the typed directive) makes the next turn replay the Slack thread into the fresh session.

### Fixed
- **The rename migration's `--verify` over-counted, and there was no way to re-run its repath.** The
  audit counted every string that CONTAINED a pre-rename path, so a finished production migration
  still reported ~56 600 "occurrences" — all of them a path quoted in a message, a tool result or a
  Codex `world_state` snapshot, plus two channel folders with a custom `workDir` that were
  deliberately never moved. It now counts STATE only: per JSON key path (Claude `cwd`; Codex
  `payload.cwd` + `payload.workspace_roots[]` — `world_state` is a per-turn snapshot Codex re-derives
  on resume, so it is content), and per existence (after the move, a recorded path that still
  resolves on disk names a directory that did not move). A project directory is stale when its name
  is no longer the encoding of its own cwd, or that cwd is gone — not when the name merely spells an
  old root. Everything else is reported in a "historical content — left by design" bucket next to
  the `events` log and never reaches the exit code, and the same key allowlist now bounds the
  REWRITE too, so a past turn's `file_path` is never edited. New `--repath` (with `--dry-run`) re-runs
  every rewrite pass against the current roots — database blobs and typed columns, config JSON,
  Claude projects + engine home, Codex index/rollouts/`config.toml`, work-folder text, service files,
  lockdown regeneration — moving nothing, idempotently, and ends with its own audit; it refuses while
  the daemon runs, exactly as the migration does. Production `--verify` now reports 0.
- **Codex fallback no longer inherits a Claude channel model**: a channel pinned to e.g. `opus`
  passed `-m opus` to the Codex fallback turn, which Codex rejects; the fallback now only honors
  Codex-family channel models and otherwise uses the gateway's Codex default.
- **/model picks in template-managed DMs were silent no-ops**: DMs default to the "User" org
  template, and `effectiveMeta` unconditionally replaced the DM's own engine/model/effort with the
  template's — so a DM `/model` pick persisted but never affected a run. A runtime pick made in the
  DM now overrides the template (empty still inherits; template model/effort don't carry over
  across a harness flip).

## [Unreleased] — `/delete` can remove user messages via an admin user token (2026-07-16)

### Added
- **Settings → Slack credentials → Admin User Token (xoxp, optional)**: when set, `/delete` also
  removes messages posted by humans and other apps — `chat.delete` is called with a per-message
  token override (a workspace admin's user token may delete others' messages where workspace
  preferences allow). Write-only (masked on read), validated to be a user token (`xoxp-…`), used
  ONLY by the org-admin-gated `/delete` — never passed to runs or MCP configs. Without it,
  `/delete` keeps its bot-messages-only behavior and the summary explains how to enable full
  deletion; with it, anything Slack still refuses is counted and reported.

## [Unreleased] — `/delete` in-thread command (2026-07-16)

### Added
- **`/delete`** (typed in a thread, org-admin only): deletes every message the bot posted in THAT
  thread — replies first, parent last — hard-scoped to the triggering channel + thread. Messages by
  humans or other apps can't be deleted with a bot token; they're counted and reported in an
  ephemeral summary instead of attempted. Refuses non-admin authors, mid-run threads (stop first),
  and top-level use; deleting a bot-owned thread root also drops the thread's session state like
  `/clear`. Covered by `test/thread-delete.test.js`.

## [Unreleased] — Admin UI redesign: #makeitfuture. brand + UX overhaul (2026-07-04)

### Changed
- **Full admin UI redesign** (`public/` only — zero API/server changes). New #makeitfuture. design
  system: self-hosted Poppins (`public/fonts/`, no CDN), orange `#fe3a02` + dark-teal token palette,
  wordmark shell, inline-SVG icons replacing every emoji in the chrome, branded login page.
- **IA restructure**: Dashboard → **Overview** (4 KPIs, orange hero cost chart with gridlines +
  dated peak); Audit → **Activity** (filterable search/channel/user/engine + paginated runs table;
  duplicate summary cards dropped for a one-line all-time strip); Channels + DMs + org templates
  merged into one **Conversations** section (grouped list with capability color dots, segmented
  All/Channels/DMs filter, fail-soft 30-day cost badges); Schedules → **Automations**.
- **Channel detail rebuilt** as Access / Tools / Runtime / Instructions / Memory: capability
  radio-cards (Full access red-treated + admin-tagged, Custom reveals raw flags), live access help,
  network switch row, filterable MCP/skills checklists with enabled counts, CLAUDE.md / MEMORY.md as
  mono editor cards. The three-save-buttons-with-a-footnote model is now ONE sticky dirty-state save
  bar (file editors keep their own Save, visibly editors); PUT payloads unchanged.
- **Users** is a table (role chips, C/S/T token state, Slack identity) with a right-side edit
  drawer; **Settings** gets a vertical section nav (Connection / Agent defaults / Integrations /
  Access & security / System) over the same single-Save contract presented as a sticky save bar with
  dirty tracking, chip editors for trusted apps + network domains, a red danger zone, and branded
  confirm/info dialogs replacing native `confirm()`/`alert()`.

### Fixed
- **Dashboard bar charts rendered empty**: `.bar-fill` was an inline `<span>` so its width/height
  were silently ignored — every "bar" was a blank track. Fills are now `display:block` and visible.

## [Unreleased] — Channel memory v2, the save loop (2026-07-04)

### Changed
- **Channel memory is now skill-packaged — nothing memory-related in `CLAUDE.md`.** `MEMORY.md`
  becomes a short budgeted index (default 3,000 chars) with `memory/<topic>.md` files for depth,
  linked as `[[topic]]` (an Obsidian-style graph inside the sandbox). The protocol (recall at task
  start, concrete save triggers, end-of-task checkpoint, consolidation) lives in a
  gateway-maintained `channel-memory` skill in each folder — refreshed write-on-change, pruned
  when memory is off. Modeled on Hermes Agent + Claude Code auto-memory; plan in
  the memory-improvement design note (internal repo). (Restored 2026-07-04 after a merge resolution dropped the
  Slice 11 code.)

### Added
- **`update_channel_memory` gateway MCP tool** — `add`/`replace`/`remove` on the index +
  `write_topic` for topic files. Daemon-side write, so saving works in every mode (read channels
  can't write files in-sandbox). Budget is enforced Hermes-style: an over-budget add fails with
  "consolidate first"; responses show a usage meter. The tool description restates the save
  triggers every turn — the always-on lever that makes the agent actually save.
- **Admin UI Memory tab** shows the budget meter and `memory/` topic files; an over-budget manual
  save is allowed but flagged.

## [Unreleased] — Durable channel instructions (2026-07-03)

### Changed
- **Channel `CLAUDE.md` is now the channel's own persistent instructions** — no longer regenerated
  every turn. The gateway upserts exactly one managed block at the top
  (`<!-- GATEWAY-INSTRUCTIONS -->`: global instructions + Slack-format guide + memory note;
  refreshed only when its content changes, self-repairing markers); everything below the end
  marker is user/agent-owned and survives new sessions, `/clear`, and settings changes on BOTH
  engines (AGENTS.md symlink). Legacy generated files migrate once — boilerplate collapses into
  the block, genuine channel text is preserved. Custom (real-project) folders are never
  block-managed. (`src/gateway/folders.js`, design in the instruction-injection note (internal repo).)

### Added
- **`update_channel_instructions` gateway MCP tool** — "add a rule that X" in Slack appends a
  standing instruction to the channel section in any mode (daemon-side write, outside the
  sandbox); `mode:"replace"` is admin-only.
- **Admin UI Instructions tab edits the real file** — the managed block renders grayed-out with an
  "edit in Settings → Behavior" link; the textarea edits only the channel section; saves are
  hash-guarded (409 on a concurrent change, e.g. the agent adding a rule) instead of clobbering.

## [Unreleased] — Claude Tag parity (2026-06-25)

A milestone informed by Anthropic's "Claude Tag" (`comparison-claude-tag.md`): adopt what fits a
confined local daemon — observability, memory, scheduling, light ambient — while deliberately NOT
copying cross-channel auto-memory.

### Added
- **Scope self-check on boot** — the daemon compares the installed app's live bot scopes (read from
  Slack's `x-oauth-scopes` response header on `auth.test`) against `slack-app-manifest.json`. When a
  required scope is missing it logs a warning and DMs every admin the exact "add these + reinstall"
  list, throttled to once per change in the gap. Because it runs on every boot it also fires right
  after `update_gateway`, so an upgrade that needs a new scope announces itself instead of silently
  half-working. (`src/slack/scope-check.js`.)
- **Usage ledger** — one normalized record per run (`usage-YYYY-MM.jsonl`) across interactive,
  scheduled, and background runs: engine, model, tokens, cost, duration. Daemon-side only
  (unreadable from a gated folder). Codex cost is estimated when an admin sets a `$/1M-token` rate.
- **Audit admin tab** — monthly totals, per-channel rollups, and a recent-runs feed over the ledger
  + run logs (`GET /api/audit`, `GET /api/audit/events`). Visibility only — no spend cap.
- **Background-job durability** — jobs persist to `config/bg-jobs.json`; on restart a live job is
  watched to completion and a job that exited while the daemon was down gets an "interrupted by
  restart" continuation, so a thread never silently stalls. (Closes the Slice 7 known gap.)
- **Live TODO checklist** — the agent's own TodoWrite plan renders as a ✓/◐/○ checklist, edited in
  place and left in the thread as a record (previously discarded).
- **Folder-scoped agent memory** — an opt-out `MEMORY.md` per channel folder that the agent reads
  and updates across that channel's threads. Single-folder (no cross-channel bleed); Claude's global
  auto-memory stays off. Non-bash channels get only a narrow `Write(MEMORY.md)` permission.
- **One-time ("run at") schedules** — `create_schedule` accepts `in_minutes`/`run_at` to fire once
  and auto-delete (e.g. "remind this channel in 2h").
- **Scheduler guardrails** — minimum recurring interval (admin setting, default 60 min) rejects
  runaway crons; per-channel enabled-schedule cap + a tick concurrency ceiling.
- **Provenance preamble** — each turn tells the agent who requested it and where (metadata, not an
  instruction), so it can address people correctly.
- **`@bot status` / `/status`** — a compact report of a channel's live background jobs, scheduled
  work, and warm/in-flight sessions.
- **Per-channel org-default token opt-out** (`meta.noDefaultTokens`) — a sensitive channel refuses
  the broad gateway-wide tokens; channel/user tokens still apply.
- **Dynamic assistant suggested prompts** — derived from the bound channel's MCP servers + skills
  (and refreshed on context change) instead of two hardcoded lines.
- **Opt-in no-response nudge** (`meta.nudges`) — one gentle reminder in a thread that has gone quiet
  past a window (default 24h). Strictly single-thread; never scans other channels.
- **App Home tab** — a read-only orientation dashboard (your access level, channels the bot works
  in, admin-UI link). No secrets.

### Changed
- The in-progress **activity log collapses** to a one-line summary on completion instead of being
  deleted, leaving a compact record above the answer.
- Slack app manifest: `home_tab_enabled`, `app_home_opened`, and the `/status` slash command.

## 2026-06-23
- Embedded **Toolbox MCP** with user / channel / org-default tokens.
- **Org-default** Composio / Skills tokens as the final fallback (channel → user → org).
- **Per-channel clean mode**: run bare for the lowest token cost (no MCP servers, skills, or
  favorites block) — closest to the model's base prompt.
- Background-jobs **auto-continue** shipped (Slice 7): the daemon owns long shell work and re-injects
  a continuation turn into the same thread when it finishes.
- **Multi-agent hygiene**: the 🤖 mention-reaction only auto-engages this agent in threads it owns.
- Settings split into 5 sub-tabs; clarified channel-access wording.

## 2026-06-22
- **Trusted bot apps** allowlist — let integrations (e.g. Make.com) trigger runs despite carrying a
  `bot_id`, still requiring a real @mention from an approved user.

## 2026-06-18 – 2026-06-19
- **Interactive permission approvals** in Slack (approve once / for this thread / forever / deny).
- **Per-channel modes**: Allow Bash (sandboxed shell + file edits), Allow Network (sandbox egress
  for `git push`/`gh`), Auto mode (autonomous, prompts auto-approved, still sandboxed); `/mode`
  command + mode badge.
- **Codex** engine integration matured: sandbox mirrors the channel mode; auto-fallback to Codex
  when Claude hits its usage/session limit; per-channel + per-thread engine selection via a
  `claude`/`codex` message directive.
- **Replies as plain Slack mrkdwn** (tables → code blocks, `##` → bold) — fewer "Show more" folds.
- **Mention-by-reaction** (react 🤖 to treat a message as a mention; configurable emoji).
- Admin UI redesign: left sidebar + master-detail Channels/DMs; channel detail sub-tabs.
- Cron schedules gained per-schedule notify targets + a "Running: <title>" announcement with the
  result threaded under it.
- Resume footer shows a copyable `cd … && claude --resume` command.

## 2026-06-17 — v1.0.0 (foundation)
- **Core gateway**: Slack Socket Mode (@slack/bolt) across DM / group DM / public / private channels;
  DM needs no mention, elsewhere requires an explicit @bot mention; spawns headless `claude -p` in a
  per-conversation **gated folder** (filesystem sandbox, MCP allowlist, persistent memory off).
- **Thread-scoped sessions** (new thread = new session; replies resume) + a **warm session pool**.
- **Per-user Composio token** injected per message author at spawn (`x-consumer-api-key`) — never
  shared, persisted to channel settings, or logged.
- **Approval-based authorization**: admins + approved (MakeItFuture-list) users; unknown users denied
  everywhere unless granted per-channel.
- **Admin web UI + REST API**: per-channel allowedUsers / allowedMcps / skills / mode; per-user
  tokens (write-only, masked); Settings page for Slack tokens, keepalive, MCP URLs — applied live
  with no restart.
- **Channel cron schedules** via a scheduler MCP tool; **admin MCP tools** to manage a channel's
  allowed servers, working folder, and modes from chat.
- **Image/file attachments** downloaded into the gated folder for Claude's Read tool; native Slack
  **Assistant status** shimmer; **stop**/`/stop`/stop-reaction to interrupt a run.
- Per-channel **Agent instructions** (`CLAUDE.md` + `AGENTS.md` symlink); Skills Manager favorites
  injected into `CLAUDE.md`; visible working folders under `~/Slack Agent/<channel>`.
- Optional **admin-UI password**; one-command installer; encrypted config **backup/restore**;
  **launchd** service with `npm run update`.