Skip to content

Changelog

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
  • 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.

  • 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.

  • 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.

  • 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.

  • 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”.
  • 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).

  • 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.
  • 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.
  • 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.

  • 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.

  • 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)

Section titled “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)

Section titled “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)

Section titled “[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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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 .”), and the documentation describes one platform.
  • 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.
  • 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 / Try 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)

Section titled “[Unreleased] — One /model wizard replaces /engine + /effort (2026-07-16)”
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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)

Section titled “[Unreleased] — /delete can remove user messages via an admin user token (2026-07-16)”
  • 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)

Section titled “[Unreleased] — /delete in-thread command (2026-07-16)”
  • /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)

Section titled “[Unreleased] — Admin UI redesign: #makeitfuture. brand + UX overhaul (2026-07-04)”
  • 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().
  • 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)

Section titled “[Unreleased] — Channel memory v2, the save loop (2026-07-04)”
  • 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.)
  • 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)

Section titled “[Unreleased] — Durable channel instructions (2026-07-03)”
  • 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).)
  • 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)

Section titled “[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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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: ” announcement with the result threaded under it.</li> <li>Resume footer shows a copyable <code dir="auto">cd … && claude --resume</code> command.</li> </ul> <div class="sl-heading-wrapper level-h2"><h2 id="2026-06-17--v100-foundation">2026-06-17 — v1.0.0 (foundation)</h2><a class="sl-anchor-link" href="#2026-06-17--v100-foundation"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore>Section titled “2026-06-17 — v1.0.0 (foundation)”</span></a></div> <ul> <li><strong>Core gateway</strong>: Slack Socket Mode (@slack/bolt) across DM / group DM / public / private channels; DM needs no mention, elsewhere requires an explicit @bot mention; spawns headless <code dir="auto">claude -p</code> in a per-conversation <strong>gated folder</strong> (filesystem sandbox, MCP allowlist, persistent memory off).</li> <li><strong>Thread-scoped sessions</strong> (new thread = new session; replies resume) + a <strong>warm session pool</strong>.</li> <li><strong>Per-user Composio token</strong> injected per message author at spawn (<code dir="auto">x-consumer-api-key</code>) — never shared, persisted to channel settings, or logged.</li> <li><strong>Approval-based authorization</strong>: admins + approved (MakeItFuture-list) users; unknown users denied everywhere unless granted per-channel.</li> <li><strong>Admin web UI + REST API</strong>: 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.</li> <li><strong>Channel cron schedules</strong> via a scheduler MCP tool; <strong>admin MCP tools</strong> to manage a channel’s allowed servers, working folder, and modes from chat.</li> <li><strong>Image/file attachments</strong> downloaded into the gated folder for Claude’s Read tool; native Slack <strong>Assistant status</strong> shimmer; <strong>stop</strong>/<code dir="auto">/stop</code>/stop-reaction to interrupt a run.</li> <li>Per-channel <strong>Agent instructions</strong> (<code dir="auto">CLAUDE.md</code> + <code dir="auto">AGENTS.md</code> symlink); Skills Manager favorites injected into <code dir="auto">CLAUDE.md</code>; visible working folders under <code dir="auto">~/Slack Agent/<channel></code>.</li> <li>Optional <strong>admin-UI password</strong>; one-command installer; encrypted config <strong>backup/restore</strong>; <strong>launchd</strong> service with <code dir="auto">npm run update</code>.</li> </ul> </div><footer class="sl-flex astro-ddtxxk7k"><div class="meta sl-flex astro-ddtxxk7k"><a href="https://github.com/makeitfutureDev/channelgate/edit/beta/CHANGELOG.md" class="sl-flex print:hidden astro-qlekgd3o"><svg aria-hidden="true" class="astro-qlekgd3o astro-hkc32dio" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.2em;"><path d="M22 7.24a1 1 0 0 0-.29-.71l-4.24-4.24a1 1 0 0 0-1.1-.22 1 1 0 0 0-.32.22l-2.83 2.83L2.29 16.05a1 1 0 0 0-.29.71V21a1 1 0 0 0 1 1h4.24a1 1 0 0 0 .76-.29l10.87-10.93L21.71 8c.1-.1.17-.2.22-.33a1 1 0 0 0 0-.24v-.14l.07-.05ZM6.83 20H4v-2.83l9.93-9.93 2.83 2.83L6.83 20ZM18.17 8.66l-2.83-2.83 1.42-1.41 2.82 2.82-1.41 1.42Z"/></svg>Edit page</a></div><div class="pagination-links print:hidden astro-b5raizh3" dir="ltr"><a href="/docs/licensing/summary" rel="prev" class="astro-b5raizh3"><svg aria-hidden="true" class="astro-b5raizh3 astro-hkc32dio" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17 11H9.41l3.3-3.29a1.004 1.004 0 1 0-1.42-1.42l-5 5a1 1 0 0 0-.21.33 1 1 0 0 0 0 .76 1 1 0 0 0 .21.33l5 5a1.002 1.002 0 0 0 1.639-.325 1 1 0 0 0-.219-1.095L9.41 13H17a1 1 0 0 0 0-2Z"/></svg><span class="astro-b5raizh3">Previous<br class="astro-b5raizh3"><span class="link-title astro-b5raizh3">Licensing summary</span></span></a><a href="/docs/support" rel="next" class="astro-b5raizh3"><svg aria-hidden="true" class="astro-b5raizh3 astro-hkc32dio" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg><span class="astro-b5raizh3">Next<br class="astro-b5raizh3"><span class="link-title astro-b5raizh3">Support</span></span></a></div></footer></div></div></main></div></div></div></div></body></html>