diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1a152850..a34d84d3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -50,7 +50,7 @@ body: id: node attributes: label: Node version - description: Output of `node --version`. The supported minimum is 22.13. + description: Output of `node --version`. The supported minimum is 24.0.0. validations: required: true - type: dropdown @@ -93,5 +93,5 @@ body: required: true - label: This is not a security vulnerability. required: true - - label: I am running a supported Node version (>= 22.13) on Linux (systemd + rootless Podman). + - label: I am running a supported Node version (>= 24.0.0) on Linux (systemd + rootless Podman). required: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5234620..f87ba545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,8 @@ jobs: matrix: # Linux only: ChannelGate runs under systemd with rootless Podman, so there is no macOS leg. os: [ubuntu-latest] - # 22.13 is the exact supported minimum (node:sqlite is stable from 22.13 — see - # package.json engines); 24 is the current LTS line. Node 20 cannot run the app at all. - node: ["22.13", "24"] + # Exercise the exact supported floor and the current Node 24 LTS patch. + node: ["24.0.0", "24"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fcef0d5..ee515ac8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: "22.13" + node-version: "24" cache: npm - run: node scripts/check-release-candidate.mjs - run: npm ci diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/AGENTS.md b/AGENTS.md index aaf653ea..ff229372 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u ## Where things live - `src/start.js` + `src/platform-gate.js` + `src/server.js` — entry and boot. `start.js` checks the - Node floor (≥ 22.13, `node:sqlite`) with NO static imports, the gate refuses every non-Linux + Node floor (≥ 24.0.0, `node:sqlite`) with NO static imports, the gate refuses every non-Linux host with one plain line, then `server.js` boots in this order: `.env`, the legacy-layout migration FIRST, the runtime root + database + singleton lock, settings over env, the skills catalog, hardening, the container CLI probe (FATAL without one), the MCP socket, Express, @@ -249,7 +249,7 @@ post/edit the reply in the thread (degraded to the surface's capabilities) → u idle reaper), exec (session-leader helpers so a later exec can signal the whole tree), credentials, the per-channel HOME volume and the mounts, the operator's VS Code lease (`npm run vscode -- `). -- `containers/` — the runtime image: `Containerfile` (`node:22-bookworm-slim`, the `agent` user +- `containers/` — the runtime image: `Containerfile` (`node:24-bookworm-slim`, the `agent` user at the daemon's real uid/gid, tini, a root-owned toolchain under `/opt/channelgate`), `versions.json` (`imageSpecVersion` + every pin: Claude Code, Codex, `mcp-remote`, `vercel`, `supabase`, Playwright + `agent-browser` with Chromium, Python with OpenCV and faster-whisper; diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d28dfa..4109f7a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,110 @@ # Changelog — ChannelGate +## 0.6.5 — candidate, pending approval + +- Show readable, redacted provider errors when a background job's automatic follow-up fails, + preserving diagnostic audit detail and protection against replaying interrupted work. + +Prepared 2026-11-06 from integrated beta development. This is not a publication date. +Requires Node.js 24.0.0 or newer and runtime image spec 1.6.3. See +[the candidate record](docs/releases/0.6.5.md) for scope, excluded unfinished work and +release gates. Stable promotion and publication require approval of the exact tested candidate. + +### Beta development notes + +- Keep Claude shell-task lifecycle updates out of subagent progress rows and the Coordinating + agents label, while preserving ordinary tool progress and genuine agent activity. + +- Preserve parsed container background-job exit codes in the structured completion audit, while + keeping completion messages in plain language and unconfirmed outcomes unknown. + +- Restore the agent directory refresh to 03:00 local time after a spring-forward clock change + instead of retaining the transition day’s normalized 04:00 hour. + +- Automatically continue the original agent thread after approving a server for a hidden variable; no manual “continue” message is needed. + +- Redact hidden proxy placeholders from detached background-shell logs before persistence, + while keeping explicitly Readable configuration visible in job output. + +- Clarify that the network tool's policy is advisory in `/sudo` direct-host threads, with no + OS-level egress cutoff. Ordinary proxy-mode container enforcement is unchanged. + +- Ship the documentation favicon so handbook pages no longer request a missing asset. + +- Revoke pending identity-link proofs for a recovered administrator, preventing an older proof + from replacing the new recovery enrollment link. Other administrators' proofs remain valid. + +- Fix horizontal overflow in the Users editor caused by stretching the personal-email sending + checkbox to the full form width. + +- Fix Sunday cron expressions using weekday `7`, including lists, ranges and steps, in schedule + validation, matching and next-run previews. Sunday `0` remains supported. + +- Add channel-to-channel agent mail: `send_agent_message` asks another channel's agent a question on the requester's behalf, gated on the requester's own access (authorization, live membership and the whole source audience, re-checked at send, delivery and reply), the target's new `acceptsMessages` profile switch and the requester's approval card. The target answers in its own channel as the rank-less `agent:` principal with a reduced toolset, and the reply returns to the asking thread as a new turn; one in flight per pair, hourly caps, restart recovery and audit events. + +- Require Node 24.0.0 or newer consistently for the daemon, installer, updater and documentation + build. Align release CI and setup guidance with the Node 24 runtime image; upgrade the host + Node runtime before updating older deployments. Refresh Astro and Starlight documentation + dependencies within their current major versions. + +- Include the official Make.com `make-cli` in every new runtime image, with per-channel + `MAKE_API_KEY` / `MAKE_ZONE` setup and destination-bound API-key protection. Refresh pinned + Claude Code, MCP remote, Vercel, Supabase and Playwright CLI versions. Existing deployments + adopt the toolchain after rebuilding the image; no host Make login is copied. + +- Detect Codex tool-process creation exhaustion after a short recovery window, preserve partial + usage without automatic replay, and record bounded runtime process/thread-limit diagnostics. + +- Extend Enterprise monthly channel spending limits with recorded-spend display, DM editing, conversation-local preservation and consistent queued/retry/fallback refusal. +- Require Enterprise for skill preferred-model switching, exact company email-domain auto-approval, dedicated channel Codex logins, external skill-catalog MCP and tokens, organization environment variables, Google Drive sync and channel skill templates. Preserve saved configuration after entitlement loss while disabling Enterprise behavior; dedicated Codex channels never silently switch accounts. +- Add Enterprise per-channel monthly USD spending limits, enforced at run admission against recorded UTC-month spend. Running and concurrent work can overshoot the cap; this is not a provider billing limit. +- SSH Claude and Codex sessions now display model, reasoning effort and context usage, with working directory, Git branch and available usage details. + +- Show a skill's preferred model in the skills library table, the chat skill listings and Slack Settings → Skills. + +- Expose "Skill model swap" in Microsoft Teams settings and as `/settings skillswap on|off` in Google Chat. + +- Rename user-facing “local skills” to “gateway-owned skills” across skill management and guidance; stored ownership values and update behavior are unchanged. + +- Queue Slack `/fork` automatically while the source thread is busy, with explicit acknowledgement, Stop cancellation and restart recovery. + +- Suppress unfinished-thread nudges and follow-up digest entries when the Slack thread root or latest reply has a ✅ reaction; thumbs-up does not close a thread. + +- Honor inherited DM network settings in the egress proxy, including live template changes, and allow legitimate HTML Slack attachments without misdiagnosing their content as a missing scope. + + +- Clear automatic skill publication recovery pins after a successful retry, including retries of unchanged edits. Preserve manual/legacy and approved override pins, protect newer edits from late publication attempts, and expose pin reasons in metadata and the library. + +- Show the new user request above the fork icon and source link in forked Slack thread roots so their topic is visible in the channel. + +- Add an optional `model` argument to `create_skill` / `update_skill` that writes or clears the skill's preferred-model frontmatter line, document it in the skill-authoring guide, and show the preferred model in the skills library. + +- Let a skill declare its preferred model (`model:` in SKILL.md): with the new per-channel "Allow skill engine and model swapping" switch on, using the skill resumes the same session on that model (sticky below a `/model` pin), or delegates to a background agent on the other engine; `run_agent_in_background` takes `engine`/`model`. + +- Charge each Claude turn only its own spend: a resumed session is no longer re-billed its history on every message, subagent tokens are counted, and every model a turn used (a Sonnet subagent under Opus) gets its own priced slice; migration 41 repairs recorded history losslessly. + +- Accept public IPv4/IPv6 destinations in SSH approval cards and raw tunnels, fixing IP-only customer connections. + +- Show host skill folders missing `SKILL.md` in the library with an issue badge, path, personal grant owners and a missing-file filter. + +- Let authorized channel members with write access approve public SSH destinations on a durable card, enabling container SSH without `/sudo`. + +- Skip granted folders without a regular `SKILL.md`, warn with the folder name, and continue with valid skills instead of failing the request. + +- Make completed `/delete` commands silent, including partial deletion; retain audit counts without posting a report. + +- Clarify catalog-first skill authoring: local skill installs are not durable; external source edits require verified repository write access. + +- Allow Composio Remote Bash alongside Remote Workbench for authorized remote processing, retaining current-run authority checks and structured membership approvals. +GPT-6.1 Terra now has Standard API-equivalent usage rates. Upgrades back up and refresh Codex history with dated rates, including previously unpriced Terra usage from October 26, 2026. + +- Fix linked-folder access exclusions and DM authorization, preserve section skills and DM settings on separation, and immediately revoke follower SSH sessions after source changes. + +- Automatically link conversations sharing a working folder to one live setup, show the source in settings, and keep harness/model choices independent. + + +- Show configured effort and a compact live activity phase beside the running model in Slack’s native Stop label. + 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, @@ -18,10 +123,149 @@ product overview. ## Unreleased +- Require Enterprise for new mandatory organization skill grants and organization-wide agent instructions. Saved policies remain active after downgrade; explicit removal remains available. + + +- Use the administrator's Slack email for optional password and Slack-code sign-in. Resolve the + address through Slack after password verification, retain stable user IDs internally, and + keep sensitive-action reauthentication bound to the signed-in administrator. + +- Fix Slack app imports: register `/status-cg` instead of the built-in `/status`, include explicit command URLs, and clarify Socket Mode setup in the installer. + +- **Company email auto-approval.** Admins can configure exact email domains in Users, preview + existing Slack users and approve matching provider identities automatically. Manual revocations + prevent automatic reapproval; no administrator privileges are granted. +- User-level skill catalog tokens: a person mints their own bearer for `POST /mcp/skills` from + chat (`create_my_skill_token`, `list_my_skill_tokens`, `revoke_my_skill_token`); it acts as that + person — organization skills, the skills of channels that admit them, their own personal skills, + resolved live, with the chat edit rules and never the library export — and dies with their + approval. Admin-minted catalog tokens remain the library for other systems and peer gateways + and never reach a personal skill in any scope. The endpoint verifies the bearer before parsing + the body and backs off invalid tokens. Migration 39. +- Channel-level user API tokens: anyone working in a conversation can create a personal run API + token for it (`create_api_token` / `list_api_tokens` / `revoke_api_token`); runs it starts are + bound to that conversation and act as the creator with their live role and authorization. The + Admin UI → API page lists and revokes every token. The organization-wide run API key is + deprecated (still works, logs a warning). +- Security hardening (run capability liveness): the daemon's MCP socket refuses a run's signed + capability on the hello and on every tool call or relayed request once its conversation has no + live work (no turn, background job or agent, memory review or SSH session, with a 30 s + post-release grace), answering a fixed sentence and auditing `capability_refused_idle`; a token + copied out of a container no longer works for its remaining 6–12 h TTL. + +- Preserve channel archival across delayed Slack joins and concurrent admin resets; reconcile missed removals/rejoins after startup or reconnect, pausing workspace updates until bot membership is verified. + +- Fix Slack List message references, attachments and URL fields so agents can populate clickable source links and read them back. + +- Fix false unavailable-skill warnings for generated operating and memory guides required by shared or personal skills. + +- Honor Readable variable visibility in reply streams, final replies and background-job output so public configuration URLs remain usable; continue redacting hidden credentials. + +- Add Contabo to the GitHub and public VPS hosting guides, including shared CPU sizing guidance and links to current provider pricing. +- Apply Free cost-history visibility to audit events, run API job costs and schedule telemetry, preserving operational records and answers. + +- Publish a VPS sizing and hosting guide in the official docs, with server tiers, concurrency, storage, and backup recommendations. + +- Archive Slack channels when the bot is removed and stop workspace configuration updates until it is re-invited. Show Archived in the admin UI and reject edits/new work for archived channels. +- Allow up to 100 active chat conversations in each channel per UTC month on Free, with 10 registered channels, 10 users and one month (30 days) of cost-tracking history. The existing 500 AI runs per channel per month remains. No-key evaluation serves one active channel with the same 100-thread allowance; Enterprise defaults to unlimited. Thread usage starts tracking on upgrade without fabricated history. + +- Add an opt-in admin sign-in alternative using the admin password and a private, expiring Slack + verification code for existing administrators, including sensitive-action reauthentication. + +- Version login-page scripts with their HTML so cached pre-upgrade code cannot reference removed fields. + +- Use the existing Users list and admin role for dashboard passkeys and invitations. Existing + keys link to their owner through private chat verification; role removal revokes dashboard + access immediately. Remove the separate dashboard account directory and retain host recovery. + +- Fix SSH and VS Code Remote-SSH selecting project storage for organization admins: connections + and refreshes now use the same current-role runtime as ordinary chat turns. Role changes + disconnect before credential refresh, login/MCP cleanup is scoped to each container, and + connection guidance explains separate admin/project history. Existing operator-home SSH + restrictions remain enforced; no history is copied or deleted. + +- Simplify admin sign-in to passkey selection without a username field. The selected credential + identifies its account; password login stays permanently disabled after first enrollment. +- Add individual dashboard admin usernames and passkey sign-in, with initial-password migration, + account/passkey management, expiring enrollment links, recent-authentication checks and + host-operator recovery. Password login stops permanently after first passkey enrollment. +- Fix signed-out admin login: allow the login script, favicon and required fonts before + authentication so a fresh browser can submit the sign-in form. +- Security hardening (audit batch 2): a channel's custom work folder can no longer be the gateway + account's home, the gateway runtime root, `~/.ssh`, `~/.claude`, `~/.codex`, the container + engine's storage, the hidden artifact trees, the ChannelGate workspace root or its platform + folders, another conversation's default folder, or a parent of any of those (another channel's + custom folder stays allowed) — refused with a named reason by the `set_channel_workdir` tool + and the admin UI, and failed closed with the remedy at spawn time for an already-stored value + (one boot warning per offending channel). Hidden, managed and credential-shaped files (`.env`, + `.claude/…`, `*.pem`, `credentials.json`, …) can no longer leave a channel through public file + links of either purpose or a previously minted link; `slack_share_file`, the footer download, + Composio staging and mail attachments refuse hidden and credential-shaped files but still send + the channel's instruction/memory files and ordinary reports; in-channel browsing and editing + are unchanged. +- Security hardening (audit batch 4): switching a channel to "Admins only" or "Locked" now evicts + the approved members seeded into its guest list (explicit guests and admins stay) from both the Slack + Access control and the admin UI, with the count in the save notice and an audit event; the + Slack Settings engine / model / effort pickers, thread pins and thread reset follow the + gateway's model-change policy like `/model` does, rendering read-only with the policy line for + anyone it excludes. +- Security hardening (audit batch 3): the login and approval-link backoff key each client on the + header of the proxy the operator declares (new Settings → Connection → *Behind a reverse proxy*, + `trustProxy` / `CG_TRUST_PROXY`: none, `cloudflare`, `xff`) instead of trusting any forwarded + header that arrives on a loopback socket, and a global cap (50 failures per 10 minutes) bounds + password guessing from fresh buckets. Deployments behind a proxy should set the new option; + without it remote logins share one backoff bucket. +- Security hardening (audit batch 1): the Slack Settings mode control no longer lets a non-admin + channel manager switch a channel into Admin mode (managers keep their other controls in an + Admin-mode channel and may step it down); case-variant URLs such as `/API/settings` are refused + by the admin guards and never routed; the admin shell and login page carry a no-inline-script + Content-Security-Policy and every response carries nosniff, frame-denial and referrer headers; + an Express error handler replaces default stack-trace responses and the systemd unit sets + `NODE_ENV=production`; `/resume` requires the admin session; the background-job "Check status" + button is gated like `/status` and scoped to its channel; file explorer share, send-to-me, + preview and edit re-prove the opened file under the channel root; the License card escapes + platform-supplied text; the internal IPC secret is compared in constant time; `npm audit fix` + moved undici, zod, hono, fast-uri, body-parser and ip-address to patched releases. + +- **Slack code channels.** When a requester asks for one, the agent can open a dedicated Slack + code channel for the task behind an approval card the requester confirms. The channel inherits + the origin channel's folder, access, secrets, skills and mode, runs as one channel-wide session + with top-level replies and no @-mention, works on a `code/` branch in a worktree when the + folder is a repository (else in place), publishes the branch's diff to the Code tab after every + turn, takes HTML, Block Kit, canvas and pull-request tabs plus context-bar items through + `update_code_channel`, honours Slack's channel-wide Stop, and archives behind a card with the + wrap-up shared back into the origin thread. The Slack manifest gains `features.code_channels` + and the `code_channels:manage` scope (paste and reinstall). `/status-cg` is accepted as an alias + of `/status` for apps whose manifest cannot register the built-in name. + +- Supply new installations with Astra, Sol, Terra, Fable, Opus and Sonnet (`:sonet`) shortcuts + using the newest supported stable family versions. Preserve customized lists and thread pins. + +- Add a default effort selector beside each harness's default model, including Qwen providers. + Inherited runs use it unless a conversation, thread or request overrides effort; blank keeps + the CLI default. Validate model compatibility and use the destination harness's effort on failover. + +- Let organization admins use another requester's Slack menu buttons and question forms. Menu + views open under the clicking admin's own identity; question answers continue the original + requester's session and record which admin responded. Current access, membership and stale-form + checks still apply. The same admin access applies to busy-thread and engine-switch/retry cards. + +- Restore Composio Remote Workbench for remote data processing and bulk app calls, preserving + workflow session and file references. Explicit Slack membership actions still require human + approval; Python inside Workbench is outside that structured gate. Other generic connector + bash/proxy/API executors remain refused. Clarify local container processing as an option for + records returned inline. + + - **Company email auto-approval.** Admins can configure exact email domains in Users, preview existing Slack users and approve matching provider identities automatically. Manual revocations prevent automatic reapproval; no administrator privileges are granted. +- Respect an explicit readable choice for an individual environment secret in strict egress + mode. CLIs that require the raw value can receive it after the existing scoped approval; + automatically readable secrets remain withheld. Secret listings now distinguish the two + cases and summarize the most specific scope. + - **Email accounts.** Connect an ordinary mailbox — IMAP for reading, SMTP for sending — to a conversation or to yourself by entering its server settings once on one form: in the admin UI (the conversation's Connections, or a person under Users), in Slack's ⚙️ Settings (MCP page for @@ -250,6 +494,10 @@ product overview. - Make Teams reply statistics smaller and add a gap above them, including replies that replace the working placeholder and background/scheduled results. +- Register Teams Graph subscriptions with literal conversation identifiers in the JSON resource + selector, while retaining URL encoding for message-fetch paths. Record the registered selector + separately so a successfully renewed subscription's provider scope is visible to operators. + - 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 @@ -2990,3 +3238,11 @@ copying cross-channel auto-memory. injected into `CLAUDE.md`; visible working folders under `~/Slack Agent/`. - Optional **admin-UI password**; one-command installer; encrypted config **backup/restore**; **launchd** service with `npm run update`. + +### Fixed — Slack Connect running indicator + +Use the sender's workspace for streaming replies in shared channels and clear the native running status after delivery, including stream failures, without allowing an older turn to clear its successor. + +### Fixed — rootless Podman system-service setup + +Keep container cgroups inside the delegated daemon service, preserve private temporary backing across restarts, and run offline resource-limit probes with distinct runtime failure diagnostics. Preserve operator Podman configuration and document idle recovery for existing stores. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c408b6c..51877c14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,8 +11,8 @@ trailer on each commit *is* that acceptance (CLA Section 7). Participation is go ## Set up -Requirements: Node **>= 22.13** (the floor is exact — `node:sqlite` is stable from 22.13 and the -daemon refuses to boot below it), Git, and Linux — the daemon refuses to boot anywhere else. +Requirements: Node **>= 24.0.0** (use the current Node 24 LTS release; `nvm use` reads +the repository's `.nvmrc`), Git, Linux, and the container runtime described in [INSTALL.md](./INSTALL.md). ```bash git clone && cd @@ -26,6 +26,27 @@ database) lives outside the repository under the gateway runtime root, not in th `INSTALL.md` covers a full working install; the tests below need neither Slack credentials nor a running daemon. +## TypeScript for new code + +TypeScript is the default for new backend and browser modules, Node scripts, and tests. Use strict +type checking and explicit types at module boundaries. Validate external inputs at runtime; types +do not replace the existing authorization checks, schemas, or adapter validation. Do not suppress +type errors with blanket `any` annotations or disabled checking. + +Adopt TypeScript incrementally. Existing JavaScript files can receive focused maintenance fixes; +when substantially extending a module, migrate it or extract the new implementation into a typed +module. A feature does not require a rewrite of unrelated JavaScript. Shell and Python helpers +keep their appropriate language. + +The current daemon and tooling execute JavaScript directly. Before adding the first executable +TypeScript module, include the mixed JavaScript/TypeScript build and execution support in that +change, with reproducible checks on the supported Node versions. Cover test discovery, browser +assets, container helper staging, installation/update/release paths, and the existing static and +security checks wherever affected. Type checking must run in CI. Do not introduce a `.ts` file +that these paths cannot execute, ship, or check; do not raise the Node minimum implicitly. This +policy establishes the language choice; it does not claim that runtime TypeScript support has +already shipped. + ## Run the checks ```bash @@ -45,7 +66,7 @@ Tests must leave no scratch directories behind. Create temp directories with `te exit-handler cleanup, and `npm test` also runs `scripts/test-scratch-sweep.mjs` first to sweep anything a crashed run left in the system temp dir. -Every one of these must pass before you open a pull request. CI runs the suite on Node 22.13 and 24 +Every one of these must pass before you open a pull request. CI runs the suite on the exact Node 24.0.0 minimum and current Node 24 on ubuntu-latest. ## Branches and review diff --git a/FEATURES.md b/FEATURES.md index df3df542..e147a3fd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,5 +1,73 @@ # ChannelGate — Features +- **Readable background continuation errors.** Failed follow-ups show a bounded provider sentence + instead of a nested JSON response. Redacted audit diagnostics retain their detail; structured + engine failure facts and the guard against replaying interrupted continuations remain unchanged. + +- **Continue after variable server approval in Slack.** Approving a hidden variable’s server automatically queues one continuation in the original thread as the original requester. It waits behind active work, honors queued Stop and current access, records usage and deduplicates repeated clicks. Queued wakeups survive restart; interrupted continuations are reported without replaying unknown actions. Denial and stale approvals do not continue. + +The documentation handbook includes its branded favicon in standalone builds and website exports. +The build validator checks that every handbook page references the shipped SVG asset. + +## Organization policy licensing + +Mandatory organization skill grants and organization-wide agent instructions require Enterprise to add or change. All HTTP, MCP and raw settings writers share the settings boundary. Saved policies continue applying after downgrade; admins can explicitly remove grants or clear instructions. Existing Lean-mode exclusions still apply; these policies do not override Lean mode. Personal/channel grants, skill content maintenance, channel instructions and core safety rules remain available on every tier. + +- **Monthly channel budget management.** The admin UI shows this UTC month's recorded spend with the Enterprise spending limit (channel Runtime pane or DM detail), unlimited by default. Limits remain local to each conversation; saves, code-channel synchronization and shared DM templates preserve them. Queued runs, retries, fallback attempts and memory reviews recheck the current cap immediately before spawning and deliver an actionable refusal. Active/concurrent runs can overshoot; unpriced usage refuses new runs until repaired, and external charges are outside the channel ledger. + +## Enterprise organization controls + +The following capabilities require an effective Enterprise license (including use of previously +saved configuration after downgrade or expiry): + +- Skill preferred-model switching: opt a conversation into honoring a skill's engine/model preference. +- Exact company email-domain auto-approval for new users and policy application to existing users. +- A dedicated Codex login per conversation, separate from the gateway login. +- The external skill-catalog MCP endpoint and its catalog/user access tokens. +- Organization environment variables shared across conversations. +- Google Drive workspace synchronization, both scheduled and on demand. +- Channel skill templates and their inherited skill grants. +- Per-channel monthly USD spending limits. + +Other tiers retain ordinary skills, channel/personal environment variables, the shared gateway +Codex login, manual user approval and standard chat connectors. Stored Enterprise configuration +is preserved when an entitlement expires; it does not activate Enterprise behavior. A channel +configured with a dedicated Codex login is refused rather than silently switched to another account. + +A spending limit is an admission control against the channel's recorded spend in the current UTC +calendar month. It is not a provider billing limit: an in-flight run can take recorded spend above +the cap, concurrent runs can overshoot, and provider-side costs are not reserved in advance. +`monthlySpendingLimitUSD: null` means no cap; a configured cap is a positive finite USD amount. +A current-month usage row with unknown cost refuses new runs until pricing is repaired or an +admin clears the cap. A saved cap also refuses new runs after downgrade; an admin can clear it +without Enterprise, so the safety control never silently disappears. +See [license keys](docs/LICENSE-KEYS.md#enterprise-organization-controls) and the regression checks +under **Enterprise organization controls** in `TEST-PLAN.md`. + + +- **SSH engine status bars.** Prepared SSH sessions give Claude a coloured footer with the live model, reasoning effort, context used percentage/window size, folder, Git branch, session cost and available 5h/7d quota usage. Missing context stays `--%` until reported; context uses the current input/cache snapshot, never cumulative tokens. Codex uses its native footer for model with reasoning, context used percentage, folder, branch and available quotas. Model/effort changes follow native live state. Settings are session-scoped, refreshed on SSH preparation; no image rebuild is needed. Reopen Claude/Codex after reconnecting to load the change. + +- **Consistent DM network policy.** The egress proxy resolves the same current organization DM template policy as engine turns. User/admin template edits take effect on the next request, including OFF overriding a stale saved ON; custom DMs and linked workspaces retain their own resolved policy. The resolver is shared without importing the run orchestrator into the proxy. + +- **HTML attachments.** Slack HTML uploads and identical HTML renamed to text download normally. The downloader rejects recognizable Slack authentication/error pages and authentication landing URLs with an access diagnostic, rather than treating all HTML as proof of a missing scope. Inspection is bounded and streamed; byte caps, channel scope and no-follow publication remain enforced. + + +- **Skill publication recovery.** Failed repository updates carry an explicit publication recovery pin, released after a successful publish retry or update (including identical-byte retries). Source sync cannot discard the protected edit; recovery resumes at the published revision. Manual, legacy and approved source-override pins remain intentional. Skill metadata reports current/effective revisions and the pin reason; the library labels publication recovery separately. + +- **Model-specific skills.** A skill may declare the model it works best with in its SKILL.md frontmatter (`model: gpt-6.1-sol`, `model: sonnet`); the engine is derived from the model and nothing else is declared. Every materialized copy of such a skill carries one instruction line asking the model to call the gateway tool `use_skill` when it starts FOLLOWING the skill (reading or comparing skills switches nothing). The live turn decides: with the per-channel switch **Allow skill engine and model swapping** (`skillRuntimeSwap`, off by default; admin UI Runtime pane, Slack Settings mode ticks, `set_channel_skill_swap` for managers, audited) on and no person's pin on the thread, a model of the SAME engine resumes the SAME session with the preferred model (the previous attempt ends on request, its spend is banked on its own model, the reply carries a "Switched this thread to …" note and the footer names the new model) and sticks on the thread as a skill preference below the `/model` pin and above the channel default; a preferred model on the OTHER engine is delegated through `run_agent_in_background`, which now accepts `engine`/`model` overrides validated against the enabled, channel-approved engines; a second skill with another model in the same turn is advised to use a subagent. A pin, the switch being off, an unavailable engine or an unknown model all answer "continue with the current model". Thread runtime reset clears the preference with the pin. Events `run_skill_swap_decision` / `run_skill_swap`. Authoring: `create_skill` and `update_skill` take an optional `model` argument that writes (or, with `""`, clears) the `model:` frontmatter line — a model-only update needs no files — refusing a model no engine recognizes; the skill-authoring guide documents the key, and the library's skill detail and table, `/api/skills`, `list_skills` / `get_skill_info` / `show_channel_skills`, and the Slack Settings → Skills page show the preferred model. Microsoft Teams settings (Access rows, channels and DMs) and Google Chat (`/settings skillswap on|off`) expose the same switch through the shared guarded settings action. + +- SSH egress approval accepts exact public IPv4 and IPv6 addresses as well as DNS hostnames. Approved literals are retained by the proxy’s raw-tunnel filter; private/loopback/metadata addresses remain blocked, and secret-host declarations remain DNS-only. + +- The admin skill library includes read-only diagnostics for host folders without a root `SKILL.md`, including folder paths and personal grant owners. Skill file status filters isolate missing files. Native plugins remain valid, and diagnostic rows cannot be enabled or assigned as catalog skills. + +- `allow_channel_ssh_host` posts a durable approval for one public SSH destination. Any currently authorized channel member with write access can approve it; no admin or `/sudo` is needed. Approval appends to this channel’s `egressRawHosts` without replacing other entries. Cards disclose supported TCP ports 22, 5432 and 6543; Auto never approves them, and network-off/private-address protections remain enforced. + +- Invalid legacy skill-folder grants are omitted from Claude plugins and the Codex personal catalog. The reply names folders missing a regular `SKILL.md`; remaining skills continue loading and source folders/grants remain intact. + +## Skill ownership terminology + +- Skills managed directly in the catalog are labelled **Gateway-owned** in the Skills catalog, owner filter, creation form, assignment picker and MCP guidance. Personal/channel/organization scope remains independent; internal `ownerKind: "local"` values remain compatible. + ## Company email domain auto-approval - Admin UI → Users → Auto-approval accepts exact company email domains, disabled by default. @@ -10,17 +78,350 @@ Editable names, message text and run-API attribution cannot establish an email identity. - Unchecking Approved in the admin user editor records a manual opt-out from auto-approval; checking it clears that opt-out. Removing domains/disabling the policy retains prior approvals. +- Every policy API request to enable auto-approval requires Enterprise, including a policy + already saved as enabled before downgrade. Rejected requests change neither domains nor audit + history; disabling the policy and manually approving users remain available. - Existing known Slack users can be previewed and explicitly approved, with fresh identity, current policy and manual opt-out checks on apply. Up to 500 users per batch; lookup failures are reported and grant nothing. Policy changes and automatic approvals are audited. +## User-level skill catalog tokens + +- **Two kinds of token for `POST /mcp/skills`.** A **catalog** token (minted by an admin in the + admin UI, as before) is the library for other systems and peer gateways: organization and + channel skills, with the existing read/propose/manage/sync scopes, and it never lists, reads, + exports or edits a personal skill. A **user** token is minted by a person for themselves with the + gateway tools `create_my_skill_token` / `list_my_skill_tokens` / `revoke_my_skill_token` (an + approved person's own Slack message; never the HTTP API principal; up to five active per person; + the value is shown once with the endpoint and a Claude Code example). It acts as that person, + resolved live on every request from the stores: organization skills (granted org-wide or + discoverable), the skills of every channel whose access policy admits them right now (the + channel's own grants, its template, its section), and their own personal skills — never another + person's personal skill, never an unauthorized channel's section skill. Its scopes are fixed to + read, propose and manage, never sync; edits follow the chat rules (a personal skill is its + author's, a channel skill its members', the organization tier an admin's); creation makes a + personal skill by default, a channel skill when `channel_id` names an admitting channel, an + organization skill only for an admin. The skill records the person, not the token, as author. A + token whose owner is no longer approved answers 403 and acts on nothing. +- **Endpoint hygiene.** The bearer is verified before the 20 MB body is parsed, invalid tokens back + off per client with the login limiter (429 + Retry-After), and `library_whoami` reports the kind + and owner. The admin UI's token table shows both kinds with the owner of a user token; admins + revoke any token there. Migration 39 adds `kind` and `owner_id` to `skill_access_tokens`. +## Channel-level user API tokens + +- **A token per conversation and per person.** Anyone authorized in a conversation asks the agent + for `create_api_token` (gateway tools `create_api_token`, `list_api_tokens`, + `revoke_api_token`; `src/gateway/api-tokens.js`, table `api_tokens`, migration 39). The value + (`cgk_…`, 192 random bits) is returned once inside the tool result with a curl example and is + stored only as a sha256 hash with a prefix and last four characters. A channel manager or admin + lists and revokes every token of the conversation; others see and revoke their own. The Admin + UI → API page lists every token across conversations with a Revoke button + (`GET /api/api-tokens`, `POST /api/api-tokens/:id/revoke`, admin session only, never a value). +- **A run acts as the token's creator.** `POST /api/runs` with a channel token is bound to the + token's conversation (`channel` may be omitted or must match, else 403), authored by the creator + as a trusted principal: their role is read live from the user store, `isAuthorized()` is + re-checked at every run (403 once they lose access), Admin-mode bypass follows the same rule as + a Slack message of theirs (admin author + Admin-mode channel), personal connections and skills + apply as in Slack, the reply menu is theirs, and work the run spawns (background jobs, + schedules, the memory review) carries them as author. The body's `author` is ignored. Status, + list and stop answer only for runs of that conversation. A revoked token answers 401. +- **The organization-wide key is deprecated, not removed.** It keeps working unchanged for + existing automations (fixed admin principal, any conversation) and logs one deprecation warning + per hour when used; the Settings card and the API page say so. +## Run capability liveness + +- **A run's control-socket capability is authority only while its conversation has live work.** + The signed capability a container run holds (`src/gateway/mcp-capability.js`) used to stay valid + for its whole TTL (6 h for a turn, 12 h for an SSH session) after the turn that minted it ended. + The daemon's MCP socket now re-checks the egress liveness registry (`src/gateway/egress/liveness.js`: + live turns, background jobs and agents, memory reviews, SSH sessions, any author) on the hello and + on every tool call and relayed request (`src/gateway/capability-liveness.js`): a validly signed, + unexpired capability whose channel has nothing live is refused with the fixed sentence "This run's + gateway capability is no longer active: its conversation has no live work." and an audit event + `capability_refused_idle` (channel, token id, tool name — never the token; one row per token and + tool per minute). +- **Grace and scope.** A call within 30 seconds of the channel's last release is still honoured, + which covers the engine's final tool call racing the daemon's release of the turn. The check is + per channel, not per token: the registry has no per-token entries, every spawn path already + registers its channel (foreground, API, scheduled, loop/follow-up/nudge and diagnosis turns + through `run.js`; background jobs and agents for their whole life including recovery; memory + reviews; the update smoke turn; SSH sessions through the broker), and the warm engine pool keeps + one process and one capability across turns, so the check refuses calls BETWEEN turns and never + kills the process. Residual: a copied token works while some work is live in that same channel, + which is the container's own trust domain. The jti-bound remote-MCP relay registry is unchanged. + The daemon-spawned stdio control server used by host (`/sudo`) turns is not covered: that run + already executes as the daemon account with the signing secret in reach, so liveness adds nothing + there. + +## Catalog-first skill authoring + +Gateway and bundled authoring instructions require catalog creation and grants. Direct engine skill-folder writes are not durable channel installations. External sources remain usable read-only; authoring in them requires verified repository and branch write access, otherwise additions live in a catalog companion skill. + +## Generated skill dependency warnings + +Generated `gateway-usage` and `channel-memory` guides satisfy shared and personal skill dependencies when their manifests are copied into the run. Missing or rejected generated sources and unrelated unavailable skills still produce the warning. + +## Environment output visibility + +- Reply streams, final replies, structured events and background-job logs honor the effective Hidden/Readable variable setting across organization, personal and channel scopes. Readable configuration such as public URLs remains shareable; hidden credentials and engine/service tokens remain redacted. A hidden value stays protected when another scope has a readable entry with identical bytes. + +## Public VPS sizing guide + +- GitHub and public hosting guides include Contabo as a budget option, with sourced plan specs, shared/dedicated CPU guidance, conservative starting concurrency, and current-pricing links. +- Official docs include `/docs/configuration/vps-sizing`, linked from installation, deployment, and the sidebar, with production/pilot/busy tiers, concurrency, monitoring, storage, and recovery guidance. + + +Codex estimates include GPT-6.1 Terra ($1 / $0.05 / $5 per million input / cached / output tokens), request-level long-context pricing, an October 26, 2026 launch boundary, and a backed-up automatic historical refresh on upgrade. + + +## Linked working-folder setup + +- Source access exclusions and conversation policy also govern linked DMs. Shared changes notify follower SSH/egress sessions immediately. Separating a follower or moving the source retains source-section skill grants; detached DMs keep their effective setup as Custom instead of reapplying a DM preset. A promoted owner remains stable when an older conversation later joins. + +- Conversations assigned to an existing folder follow its established owner live: skills/template and scoped catalog skills, memory, instructions, access, connectors/channel secrets, VPN service metadata, mode/Auto/Lean/network and Drive setup. Shared edits direct users to the source; harness/model/effort/authentication and personal grants remain independent. +- Admin and Slack settings identify the source; the folder picker explains linking. Durable files use owner identity/platform; cross-platform turns receive actual surface capabilities separately. Drive sync has one owner. Unlinking retains the effective setup; owner departure promotes a remaining conversation with its saved setup. + + +Slack’s native running/Stop label shows the resolved model and explicitly configured effort, plus compact live phases (Thinking, Using tools, Writing, Waiting, Queued, Coordinating agents). Engine fallback replaces both model and effort; unset effort is omitted rather than guessed. + +Claude task lifecycle messages distinguish shell work from subagents: explicit non-agent tasks, +including foreground and background `local_bash`, never create agent activity rows or the +Coordinating agents phase. Their later untyped lifecycle messages retain that classification, +including tool-use aliases. Ordinary Bash tool events and genuine local/remote Agent or legacy +Task progress remain visible; older agent streams without optional task-type metadata still work. + +## Optional password and Slack code sign-in + +- Settings → Admin access can enable an alternative to passkeys: the configured admin password + and the administrator’s Slack email address, followed by an eight-digit code delivered + privately to that existing Slack administrator. Email lookup uses the configured gateway Slack + app and requires `users:read.email`; user-entered or editable stored email never grants access. + Slack IDs remain the internal identity, and passkey selection is unchanged. + The option is off by default; password alone never grants dashboard access. Existing passkeys + remain available. A recently verified, identity-linked initial setup can enable this option + without first registering a passkey. +- Codes expire after five minutes, are bound to the requesting browser, accept at most five + guesses and are single-use. Delivery failures, role revocation, password changes and disabling + the option invalidate pending authority. Sensitive actions can repeat password and Slack + verification. Sessions retain the normal idle/absolute expiry and current-role checks. + +## User-linked dashboard passkeys + +- Secret reveal audit events retain the administrator account actor and include its canonical + linked user ID for successful, denied and rejected requests. They never include secret values, + submitted passwords or caller-supplied identity claims. +- The existing **Users** directory and its admin role govern dashboard access. Each credential + container links to one stable user ID; there is no separate dashboard user directory. Users → + an admin's details issues a 15-minute single-use enrollment link or revokes sessions. Removing + the admin role immediately invalidates sessions, invitations and pending authenticator results; + restoring the role does not revive old authority. Non-admin and synthetic API users cannot enroll. +- Sign-in needs only a saved passkey, with required device PIN/biometric verification. The gateway + checks signatures, exact configured HTTPS origin/RP ID, single-use five-minute challenges, + browser/session/ceremony binding and credential counters. Public options disclose no credential + directory. Sessions expire after 30 minutes idle or 12 hours total. Sensitive changes require + passkey authentication within five minutes; Settings → Admin access manages your own keys. +- Existing keys remain intact during migration. Their owner selects an existing admin and proves + control using an eight-digit private chat code, valid five minutes and bound to that browser + session, with five attempts maximum. Matching names never assign ownership. Unlinked accounts + can complete this verification but cannot use ordinary admin APIs once known admins exist. + Initial password setup follows the same proof before enrollment; fresh installs without known + admins retain temporary operator setup. First enrollment retires the password-only bootstrap path; the optional password plus Slack code flow requires explicit enablement. +- Host operators recover a known admin with `npm run admin:recover -- --user `. + Recovery revokes its keys/sessions and every pending identity proof targeting that user, + including proofs issued from another unlinked credential container. Other users' proofs remain + valid. It prints a short-lived registration-only link; it never + restores password login. Private keys remain with authenticators; SQLite stores public keys + and verification metadata. Your final own passkey cannot be deleted through ordinary management. + +## Sign-in assets before authentication + +- Login HTML is not cached and versions both classic scripts with a shared content hash, preventing stale edge/browser scripts from expecting fields removed by a deployment. + +- The signed-out admin page serves its login script, favicon and three Poppins font files without + a session. These exact public paths prevent authentication redirects from replacing JavaScript + and fonts with login HTML; other dashboard assets and privileged APIs remain session-gated. + +## Work-folder boundary and protected export names (security audit batch 2) + +- **Sensitive roots are never a work folder.** Whatever the allowlisted `fsRoot` admits, a channel's + custom work folder may not be the gateway account's home directory itself, the gateway runtime + root or anything inside it (database, config, secrets), `~/.ssh`, `~/.claude`, `~/.codex`, the + container engine's configuration or storage, the hidden runtime artifact trees, the + `~/ChannelGate` workspace root or one of its platform folders, another conversation's DEFAULT + folder (`~/ChannelGate//`, which carries that channel's uploads, memory and + lockdown file), or a parent of any of those (`src/gateway/workdir-boundary.js`). Another + conversation's custom folder, its parent and its subfolders stay allowed: two channels pointed + at one repository by their admins is an explicit, legitimate choice. A channel's own folders are + always allowed. Enforced at every door with a named reason: the + `set_channel_workdir` tool, the admin UI's Working folder field, and the runtime resolver, which + refuses to spawn a turn, background job, schedule or review for an already-stored forbidden value + and names the remedy (`set_channel_workdir` / Settings → Working folder / `clear_channel_workdir`). + Folder provisioning falls back to the default folder rather than writing into such a tree, and + the daemon logs one warning per offending channel at boot. +- **Protected names never leave the channel.** One export door (`openConfinedFile`) serves every + way bytes leave the folder, at two strengths. The strict form guards the UNAUTHENTICATED + destination — `create_public_file_link` (both purposes) and the public-link fetch route — and + refuses a hidden file or directory anywhere in the path (`.env`, `.claude/…`, `.git/…`, + `.github/…`), the managed `CLAUDE.md`/`AGENTS.md`/`MEMORY.md` and `.claude` tree, and every + credential-shaped or secret-mentioning name (`*.pem`, `*.key`, `id_rsa`, `credentials.json`, + `*token*`, `*secret*`, …). The lenient form guards the doors an authorized person drives into a + place they already reach — `slack_share_file` into the thread, the Slack footer file download + to their browser, `stage_file_for_composio` and mail attachments through their own account — + and refuses every hidden path and credential-shaped file (`.env`, `.claude/…`, `*.pem`, + `credentials.json`, `id_rsa`, …) while letting the channel's instruction/memory files and a + report that merely mentions a token through. Both the requested path and the physical path of + the opened descriptor are checked, so an internal symlinked directory cannot launder one, and a + link minted for such a file before the rule stops serving. + Browsing, reading and editing inside the channel are unchanged (`.env` stays editable in the + file explorer; it simply cannot be exported), and the Teams and Google Chat file windows keep + previewing every contained file for the authorized requester. +## Access policy and runtime controls consistency (security audit batch 4) + +- **"Admins only" and "Locked" lock the channel the moment they are saved.** Under the default + "Approved members" policy every approved member is seeded into the channel's guest list so the + Access page can show them; the run gate honours that list before the policy. Narrowing the + policy — from the Slack Access control or the admin UI's channel save — now evicts those seeded + (approved, non-admin) users from the list inside the same metadata transaction and keeps the + explicit guests (un-approved users), which remain the one deliberate way into a restricted + channel, and the admins (admitted by role under "Admins only"; under "Locked" the admin who + locks the channel keeps their own grant instead of locking themselves out of its controls). The + Slack save notice reports how many members were removed, an audit event + (`channel_access_members_evicted`) records the count only, and widening the policy back does not + re-admit anyone by itself (seeding resumes on the next message). The org-wide access reset still + clears the whole list. +- **Settings runtime controls follow the model-change policy.** The channel engine / model / effort + pickers, thread pins and the thread reset in Slack Settings apply the gateway's model-change + access exactly like `/model`, the wizard, the shortcuts and the platform controls: a DM owner may + always set them; elsewhere a non-admin may only under the "users" policy. Someone who may not + change them sees the current values and the 🔒 policy line instead of pickers, and a crafted + payload is refused with the same message and writes nothing. +## Login backoff behind a reverse proxy (security audit batch 3) + +- The admin login backoff and the approval-link token backoff key each client on the header of + the proxy the operator DECLARED, never on a header a loopback socket happened to carry. Settings + → Connection → *Behind a reverse proxy* (`trustProxy` in settings.json, `CG_TRUST_PROXY` in + `.env`; the setting wins) takes `""` (none: the connecting address is the client), + `cloudflare` (`CF-Connecting-IP`, which cloudflared overwrites on every request) or `xff` (the + LAST `X-Forwarded-For` hop, the one the trusted proxy appended, never the first, which the client + typed). The legacy `CG_TRUST_PROXY=1` means `xff`. An unknown value trusts nothing; the admin API + refuses it with 400. +- A global failure cap bounds guessing independently of the key: more than 50 failed attempts in + ten minutes across all clients puts every login on the maximum delay until the window drains, so + a caller who can mint fresh buckets (a botnet, or a forged header behind a misdeclared proxy) is + still bounded, and so is the scrypt work an unauthenticated caller can cause. +- Documented in `docs/OPERATIONS.md` → Reverse proxies and tunnels and in INSTALL.md: without the + setting, all remote logins behind a proxy share one backoff bucket. +## Web and Slack surface hardening (security audit batch 1) + +- **Admin mode is admin-only on every surface.** The Slack Settings mode control refuses to switch + a channel into Admin mode unless the clicker is a current organization admin, the same rule + `/mode admin` and the `set_channel_admin_mode` tool already enforce. A channel manager keeps + every other control in a channel that is already in Admin mode (changing Auto, Lean or the + network switch leaves the mode alone) and may step the channel down to Worker or Read-only; the + "Admin" option is listed only for someone who may enable it or when it is the current mode. +- **Case-variant URLs never reach the admin router.** Express case-sensitive routing is on, and the + Host/Origin guard, the no-password lockdown, the 401 branch and the run-API key gate all compare + lowercased paths, so `/API/settings` is refused exactly like `/api/settings` instead of slipping + past them into the router. +- **Security headers.** Every response carries `X-Content-Type-Options: nosniff`, + `X-Frame-Options: DENY` and `Referrer-Policy: same-origin`. The admin shell and the login page are + served with a Content-Security-Policy that admits same-origin scripts only plus the import map + by content hash (no inline script anywhere; the login handler lives in `public/login.js`), + same-origin styles with inline attributes, same-origin fonts, images and connections, + `frame-ancestors 'none'`, `object-src 'none'`. +- **No stack traces.** One Express error handler answers unexpected failures with a bare 500 (logged + server-side) and keeps exposable client errors such as a malformed JSON body as their 4xx with + the message only; the systemd unit also sets `NODE_ENV=production`. `/resume` requires the admin + session; the `/internal/*` IPC secret is compared in constant time. +- **Background-job status button.** "Check status" on a background job card runs through the same + authorization gate as `/status` (admin, approved, or an explicit guest of that channel) and only + reports a job that belongs to that channel; the log-tail fallback reads the channel's own log. +- **File explorer exports re-prove the file.** Share, Send to me, Preview and Edit open the resolved + file `O_NOFOLLOW` and re-resolve the kernel descriptor through `/proc/self/fd`, refusing a file + that no longer sits under the channel root, which closes the symlink-swap race the agent-side + export tools already closed. +- **License card.** The organization, tier and limit values from the licensing platform are + HTML-escaped before rendering. +- **Dependencies.** `npm audit fix` applied: undici, zod, hono, fast-uri, body-parser and + ip-address moved to their patched releases; `npm audit --omit=dev` reports no vulnerabilities. + +## Slack code channels (2026-10-25) + +- **A code channel is a dedicated Slack channel for one task**, opened by the agent through + Slack's code-channel API (`agents.conversations.*`, manifest `features.code_channels`, scope + `code_channels:manage`). The whole channel is one agent session: people talk to the agent there + without @-mentioning, replies are top-level channel messages (or stay in a thread a person + started), Slack shows a channel-wide **Stop** button, a **Code** tab with the task branch's diff, + up to five artifact tabs and a context bar (folder, branch, repository, pull request). +- **Only on explicit request, behind a human click.** The agent calls `open_code_channel` only + when the requester asks for a code channel; the call posts a durable approval card in the origin + thread that the requester (or an admin) confirms. Nothing is created before the click. On approval + the daemon creates the channel linked to the origin message (members of the origin channel may + join; privacy follows the origin unless forced), registers it, sets the session active, pins the + context bar and starts the first turn there itself with the task as the brief and the origin + thread as context. Scheduled and background runs, DMs and code channels themselves cannot open + one. Slack's eligibility refusals (`feature_disabled`, `user_not_enabled`, `restricted_action`, + a missing scope, a Slack Connect or DM origin) are reported by name with the remedy. +- **A child of the channel it came from.** The code channel inherits the origin channel's work + folder, access policy and guest list, environment secrets, skills and template, engine, model and + every mode switch — re-projected from the origin's current meta on every inbound message and + every spawn, so a rotated secret or a narrowed policy in the origin reaches the code channel at + once. It keeps its own Slack identity, sessions, container and HOME volume; it mounts the same + host folder. The origin's gateway-owned default folder is accepted as its folder regardless of + the allowlisted filesystem root; a custom origin folder still passes containment. +- **Files: a task branch by default.** When the origin folder is a git repository the daemon + creates branch `code/` in a worktree at `.worktrees/` inside it (from the checkout's + upstream, else its exact HEAD commit; `.worktrees/` is added to `info/exclude`; a taken branch + name gets a numeric suffix), and the run preamble tells the agent to work only there. A folder + that is not a repository works in place. The requester can force either (`mode`: + `worktree` / `in_place`). After every turn the daemon publishes the worktree's unified diff + against the base to the Code tab (committed and uncommitted tracked changes; the gateway's own + managed instruction block in CLAUDE.md/AGENTS.md is left out; capped under Slack's 1 MB). + Archiving never deletes the worktree. +- **Artifacts from inside the channel.** `update_code_channel` publishes or replaces a tab by key + (`html` self-contained document, `block_kit` view, `canvas` by id, `pull_request` by URL — which + also adds a Pull request link to the context bar), adds context-bar items (five in total), renames + the session, or forces a Code-tab refresh. It refuses outside a code channel and in an archived one. +- **Finishing.** `archive_code_channel` posts the wrap-up (shared back into the origin thread by + Slack), closes the session and archives the channel behind an approval card anyone working in the + channel can confirm; the row records `archivedAt`. The native Stop button without a thread + timestamp stops everything running in a code channel; outside one such an event is ignored. +- **`/status-cg` alias.** Slack rejects `/status` in some app manifests because it is a built-in + Slack command; the gateway registers the same status report under `/status-cg` as well, so a + shipped manifest registers `/status-cg` and supplies explicit HTTPS placeholder URLs + for Socket Mode imports. `/model` replaces the retired `/engine` and `/effort` commands. +- **Surfaces and declarations.** `codeChannels` is a Slack-only platform capability; the tools do + not register on other surfaces. The `gateway-usage` guide gains a Slack-only + `references/code-channels.md` and a routing row. Memory review runs for threaded code-channel + turns only; channel-level status writes skip the legacy thread bridge. + → TEST-PLAN: Slack code channels. + +## Admin access to Slack controls + +- Current organization admins can open Files, Variables, Settings and legacy menu Resume controls + on another requester's message or `/menu` card. Each new settings/file/variable view belongs to + the admin who clicked; personal credentials, connections and skills use that viewer's identity. + Current channel authorization and membership are checked, and ordinary members remain bound to + their own controls. Private modal navigation and submissions remain bound to their viewer. +- Admins can open, answer, page, submit and cancel another requester's pending question cards and + forms. Both people must retain channel access and membership, and the responder must remain an + admin. Revision checks and single submission still apply. Continuations retain the original + requester's runtime identity and credentials; audit records identify the responding admin + without storing their answer text. +- Admins can also resolve shared busy-thread Steer/Queue/Cancel and engine-switch/retry cards + after current rank, membership and channel access checks. The stored incoming event retains its + author, credentials and runtime permissions; accepted decisions audit the responding admin. + Expiration, duplicate claims and the cross-author automatic steering guard still apply. + ## Registered inventory limits and Enterprise harness approvals -- Non-Enterprise installs allow at most 20 registered channels (including DMs and the API - conversation) and 20 stored human users, including unapproved accounts. Registration checks +- Non-Enterprise installs allow at most 10 registered channels (including DMs and the API + conversation) and 10 stored human users, including unapproved accounts. Registration checks are atomic; existing records remain editable and usable when an install is already over its cap. - Legacy free-license payloads cannot bypass these caps. The no-key monthly one-conversation - limit and Registered 500-message monthly limit remain. Enterprise defaults to unlimited. + Legacy free-license payloads cannot bypass these caps. The no-key monthly one-active-channel + evaluation remains. Free allows 100 active chat threads per channel per UTC month, with + 500 AI runs shared by that channel each month. Enterprise defaults to unlimited. Thread + tracking starts with post-upgrade activity; existing AI-run accounting is retained. - Enterprise organization admins can approve one or more harnesses per channel through the admin editor or Slack Settings → Agent. Unrestricted is the default. Pins, saved sessions, API overrides, automation and fallback cannot launch outside the approved set; queued work @@ -39,7 +440,9 @@ recommends a channel without sending messages, delegating or transferring files. Organization skills can supply company terminology; no company directory is hardcoded in the product skill. - Built-in reconciliation refreshes channel metadata at startup and nightly at 03:00 in the host - local timezone without an engine turn or user schedule. Metadata caches narrow keyword searches; + local timezone without an engine turn or user schedule. A missing spring-forward 03:00 runs at + the first valid local time and the next day returns to 03:00; autumn runs once per local day. + Metadata caches narrow keyword searches; returned recommendations still verify live metadata/membership and fresh gateway access. The daemon registers its existing Slack lifecycle manager in the live connector registry; disconnects fail closed and reconnects resolve the current client without retaining old readers. @@ -49,6 +452,34 @@ A one-to-one bot DM uses its requester's access. Admin status never substitutes for membership. Slack supplies the membership interface; unsupported/disconnected platforms fail closed. +## Channel-to-channel agent mail + +- `send_agent_message` lets one channel's agent ask another channel's agent a question on the + requesting person's behalf (`src/gateway/agent-mail.js`). Calling it posts a durable + control-plane card in the asking thread; the requester (or an admin) confirms the exact text, in + Auto mode too. On the click the daemon opens a new thread in the target channel quoting the + message with the source channel and requester named, runs the target's agent on it as a normal + turn in its own container, folder, mode, memory and secrets, posts the reply there, and brings + the reply back into the asking thread as a new continuation turn of the origin session. +- A message may only go where the requester could post themselves: authorized on the target under + the gateway's access rules AND a live member of it (admins do not skip membership), with every + member of the source conversation also belonging to the target because the reply lands there. + The gate is the agent directory's verifier, re-run when the tool is called, right before the + target turn spawns, and before the reply is re-injected; a lapsed check withholds the reply and + says so. DMs cannot send; hidden, disabled, archived and Lean channels cannot receive; the target + manager must switch `acceptsMessages` on via `set_channel_agent_profile` (off by default). +- Reachability is never authority. The delivered turn runs as the fixed `agent:` + principal: no personal Composio/secret/skill scope, no admin or manager rank, never the API + principal's standing, the reduced `agent-message` toolset (workspace reads, channel memory, this + channel's Slack tools, permission cards) and no further `send_agent_message`, `search_agents`, + background work, schedules, questions or admin switches. API runs and delivered agent turns + cannot send. Permission cards name "The agent from #" and mint no personal links. +- Guards: one message in flight per asking thread and target channel, at most 3 per pair per hour + and 20 per source channel per hour, text only (≤ 8000 characters) with Slack control sequences + defanged in the quoted message, a 30-minute ceiling on the target turn, redaction at both ends, + `agent_messages` rows (migration 43) and `agent_message_*` audit events. A gateway restart fails + interrupted exchanges closed in both threads (never re-runs them) and redelivers answered ones. + ## Composio connection visibility - Slack Settings → **You** shows the viewer's personal connected Composio applications and @@ -82,11 +513,13 @@ - Container and host runtime connections, including SDK sessions, traverse the same daemon check. Denial, timeout, unavailable approval, cancellation, disconnect or expired/revoked authority prevents forwarding. Ordinary structured reads and writes continue. -- Opaque Composio workbench/bash/proxy/API executors are refused because arbitrary persistent - code can hide a membership change. Use structured connector actions instead. This enforcement - covers managed Composio; independently supplied credentials, custom MCPs and browser sessions - remain outside that boundary. The always-loaded guide forbids bypass through any route and - treats sharing/posting requests as separate from permission to join or change the sender. +- Composio Remote Workbench and Remote Bash are allowed for remote result processing and bulk app calls, with + code/commands and workflow session references forwarded unchanged. Code and app calls inside them are + outside the structured membership gate; the guide requires structured actions and human + approval for membership changes. Other generic connector proxy/API executors remain + refused. Independently supplied credentials, custom MCPs and browser sessions also remain + outside that gate. The always-loaded guide forbids bypass through any route and treats + sharing/posting requests as separate from permission to join or change the sender. ## Google Chat native conversation controls @@ -628,6 +1061,9 @@ control-plane status. The operational audit distinguishes Graph callback receipt, validation, authenticated target acceptance, rejection and retryable failures using bounded metadata. Credentials, validation tokens, message bodies and untrusted resource identifiers are excluded. + Graph subscription JSON uses literal Teams channel/chat identifiers; HTTP message-fetch paths + encode individual path segments. The subscription store records the registered selector without + changing activation cutoffs or event identities. ## Claude login for nested commands @@ -648,6 +1084,14 @@ control-plane status. ## Gateway model shortcuts +New installations inherit `:astra`, `:sol`, `:terra`, `:fable`, `:opus` and `:sonet` from the +engine model catalogs. Each resolves to the numerically newest stable version of its family, +including Sonnet 5.5 and GPT-6.1 Sol when offered by the catalog. Live Codex discovery can advance +these defaults; Claude uses the curated supported catalog shipped with the gateway. Missing +families are omitted instead of guessed. A stored shortcut map, including an intentionally empty +map, is preserved exactly. Saving unrelated settings keeps inherited defaults; editing the shortcut +list makes it an explicit set of pins. Existing thread pins never follow catalog updates. + Admins can add gateway-wide model shortcuts in Settings → Access & security → Model shortcuts. Each name selects an engine and model. In Slack, `@agent :astra task` pins that engine and model to the current thread and runs the task; `@agent :astra` switches the thread without starting an agent turn. Slack's automatically inserted space after the mention is accepted. Changing a mapping affects later shortcut selections, while threads already pinned keep their selected model. Channel runtime-change access and dedicated Codex-login restrictions apply. ## Public documentation website @@ -899,11 +1343,7 @@ revocation removes future runtime grants. Source path/symlink/size validation re revisions on failed sync. Details and compatibility limits: `docs/SKILLS.md`. -- **Slack reports shared-folder conflicts:** an authorized mention or typed command that cannot - start because channels share a folder with conflicting skills or memory settings gets a reply - in its triggering thread. The reply asks an admin to assign separate folders or align settings, - then retry. It exposes no other channel names or filesystem paths. The conflict still blocks - workspace writes and engine startup. → TEST-PLAN: Slack shared-folder conflict replies. +- **Shared-folder conversations link automatically:** incompatible initial skills/memory adopt the established source setup. Settings identify the source, and divergent metadata edits explain where to edit. Engine/model choices stay local. → TEST-PLAN: Linked working-folder setup acceptance. - **Read/search account routing:** unrestricted information requests may search either or both available Composio identities without an account-selection interruption. Explicit account and @@ -1127,7 +1567,8 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. pending product decision, not shipped). - **Secret handling:** masked dialogs collect password proof. Known credential values are redacted from primary/fallback replies, stream events, errors and background delivery checkpoints. Shell - jobs also redact injected channel secrets inside their detached container process before stdout + jobs also redact injected channel secrets and their hidden proxy placeholders inside their + detached container process before stdout or stderr reaches the durable artifact log. Independent stream buffers protect split values, and the wrapper survives the daemon while preserving the command's terminal status. The wrapper receives secret names only; jobs do not inherit engine service credentials. Existing historical @@ -1281,7 +1722,7 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. memory body. `search_channel_memory` rebuilds and queries a derived SQLite FTS5 index and `read_channel_memory` loads one validated source; both handlers return through the injected MCP response formatter. Writes no longer fail on a character budget. The index is optional per - engine build: a Node whose SQLite lacks FTS5 (22.13, the floor) still boots, and search falls + engine build: a supported SQLite build without FTS5 still boots, and search falls back to a plain scan with the same AND semantics and excerpts until the index exists. → TEST-PLAN: Conversation settings + on-demand memory. @@ -2078,9 +2519,8 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. `/delete` (org-admin only: wipe THIS thread — replies first, parent last — hard-scoped to the triggering channel+thread; the bot token deletes the bot's own messages, and when an optional Admin User Token (xoxp) is set in Settings → Slack credentials, everyone else's are deleted via a - per-call token override too — workspace prefs permitting; anything Slack refuses is tallied + - reported in the ephemeral summary, which also says how to enable full deletion when no user token - is set; refuses non-admin authors, mid-run threads, and top-level use; a deleted bot-owned root + per-call token override too — workspace prefs permitting; anything Slack refuses is tallied in the audit log; completed deletion posts no summary + (neither ephemeral nor regular), including partial deletion; refuses non-admin authors, mid-run threads, and top-level use; a deleted bot-owned root also drops the session like `/clear`), and `/update` (admin-only — git pull + install + restart, detached). → TEST-PLAN: In-thread commands. - Stop an in-flight run four ways: Slack's **native Stop button** on the thread (the agent session's @@ -2166,6 +2606,13 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. Shutdown then performs its existing bounded drain and sweeps every tracked cold and warm process group (with a final hard-kill fallback). Completed rows are cleared before exit while genuinely interrupted rows remain recoverable on boot. → TEST-PLAN: Automation. +- Recurring cron weekday fields accept both `0` and `7` for Sunday, including lists, ranges and + steps. Steps apply before Sunday normalization; matching and next-run previews agree. +- Both `list_schedules` forms show the next nominal due time in the gateway timezone. Detail + responses add derived `nextRun` (UTC ISO) and `nextRunLocal` (zone-labelled display) fields; + cron previews are recalculated at read time without rewriting the stored expression or state. + Disabled or unresolvable schedules have null previews. One-time and interval schedules use + their saved due time, including overdue times; previews do not guarantee delivery or admission. - 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") alongside recurring cron; recurring crons are held to a minimum interval (default 60 min) with per-channel + concurrency caps. @@ -2217,6 +2664,11 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. Pending acks persist in `config/acks.json` so the chain survives a daemon restart. The renderer owns the "⏰ *Reminder:*" label: one leading "Reminder:" the author already wrote is stripped from the posted message, the 2nd notice and the DM, so the line never stutters. +- Slack thread completion: no-response nudges, scheduled follow-up digests and on-demand + pending reports check current reactions before delivery. A `white_check_mark` (✅) on the + root or latest reply suppresses the thread for all recipients; 👍 does not. Root checks remain + effective after newer replies. Reply checks apply while that reply is latest. Long threads are + paginated; unreadable reaction state defers notifications until a later attempt. - Personal no-response nudge: each user chooses whether the bot posts one gentle reminder addressed to them when their last agent reply has gone quiet past a window (default 24h). The live user preference follows them across channels and DMs and is self-service in Slack App Home; admins can @@ -2253,7 +2705,10 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. can spin up a tasklist (default **Name** + **Status** New/In progress/Done, or `todo_mode`/custom columns) and read/add/update rows. Composio has no Lists coverage, so these call the Slack Web API (`slackLists.*`) directly with the workspace bot token (needs `lists:read`/`lists:write`); text cells - are wrapped as rich text and select values accept the option label. `list_id` accepts a raw F-id or a + are wrapped as rich text and select values accept the option label. Message columns accept source + permalinks as native clickable references, attachment columns accept existing Slack file IDs, and + link columns accept URLs or labeled link objects; reads retain their URLs and file IDs. Empty arrays + clear these typed fields. `list_id` accepts a raw F-id or a pasted List URL. A newly created standalone List is immediately granted write access to the current conversation, so members can see and edit it; sharing failure is reported instead of returning an invisible tracker. Available to any allowed user (no admin gate). → TEST-PLAN: Slack Lists. @@ -2431,7 +2886,8 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. resume the old one's conversation), and replays the Slack thread context into it so the new engine continues the conversation instead of starting blind. - Slack `@agent /fork ` branches a completed Claude or Codex container session using - the engine's native fork command. The bot posts a new top-level message linking to the source, + the engine's native fork command. The bot posts a new top-level message with the user’s new request first and the + `↪️ Fork of this thread` source link underneath, posts the new request in that thread, links back from the source, and runs the request in the forked session. The source session keeps its ID and history; the new Slack `thread_ts` maps to the engine's distinct child ID. Engine, model, effort and clean-mode pins carry into the branch. @@ -2440,11 +2896,17 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. access policy as ordinary shortcuts. A shortcut for a different engine starts a fresh session in the linked child thread and replays the source's Slack messages through the existing bounded context handoff. It does not transfer the old engine's native transcript/tool state. The fork - command and later source messages are excluded; unavailable Slack history is refused before + command and later source requests are excluded; queued cross-engine forks include bot replies completed while waiting; unavailable Slack history is refused before creating the child. Clean threads stay bare without history replay. Dedicated channel Codex logins stay on Codex. Unknown, disabled or invalid targets and missing request text are refused before a child thread is posted. - A running or sudo/source-host session is refused until it can be forked safely. + A running source queues the fork automatically, acknowledges it explicitly, and starts it after preceding work finishes. Stop cancels queued forks. Admission and session state are rechecked at promotion; queued forks survive restart through active-run recovery. Sudo/source-host sessions remain refused. +- Gateway-wide **default effort** beside each harness's default model (Settings → Agent defaults → + Engine & runtime, including each Qwen provider card). Choices follow the selected model's + supported effort levels; saves validate the model/effort pair atomically. Per-request, thread + and channel/DM-template effort overrides take precedence. Inherited fresh and resumed turns use + the harness default; blank preserves the CLI default. Failover uses the destination harness's + own default effort, and model retries recheck compatibility before passing an effort flag. - Gateway-wide **default model** per engine (Settings → Engine & runtime, validated like `/model`): when a thread/channel/DM-template sets no model, the run gets the gateway default as an explicit `--model`/`-m` — so the admin's interactive terminal `/model` choice (written to @@ -2642,8 +3104,12 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. channel state remain reachable through its authorized tools only. - **CLI catalog (`src/config/cli-catalog.js` — Vercel, Supabase, Make.com)**: the deploy CLIs the channel runtime image ships, with the environment variable each one accepts instead of a saved - login. It feeds the `/secrets` name suggestions (every catalog name, so a channel acting as its - own account types `SUPABASE_ACCESS_TOKEN`, not a guess) and the host-sandbox write-deny list for + login. Every newly built runtime image includes the official `@makehq/cli` as `make-cli` + (spec 1.6.3), pinned in `containers/versions.json` and installed root-owned. Make CLI + authentication uses the channel's `MAKE_API_KEY` and `MAKE_ZONE` (for example `eu2.make.com`); + the API key remains a destination-bound proxy placeholder and the zone remains routing + configuration. Host Make logins are never copied into containers. The catalog feeds the + `/secrets` name suggestions (every catalog name, so a channel acting as its own account types `SUPABASE_ACCESS_TOKEN`, not a guess) and the host-sandbox write-deny list for saved-login files. **Retired 2026-09-03 (Linux + containers only):** the Settings → Network → "CLI integrations" switch, its `cliIntegrations` setting, the live "installed" badges (`cli-detect.js` detection) and the read-only linking of the daemon's shared host login @@ -3009,7 +3475,7 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. application-code change is needed. → TEST-PLAN: ChannelGate skill package. - **Trustworthy release gate** (the macOS leg retired 2026-09-03 — Linux only): every pull request - runs the complete coverage-gated suite on Ubuntu at the exact Node 22.13 minimum and Node 24, plus dependency-free syntax / + runs the complete coverage-gated suite on Ubuntu at the exact Node 24.0.0 minimum and current Node 24, plus dependency-free syntax / whitespace checks, tracked-file secret scanning, production dependency audit, and independent coverage floors for authorization, access-grant isolation, sandbox policy, secret handling, and updater state. Claude @@ -3274,6 +3740,9 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. `run_config` event records `networkEnforced` and `egress` beside `networkPolicy` so an operator reading it after an incident cannot mistake one for the other. `NETWORK_POLICY_ENFORCED` in `src/engines/network-policy.js` is now `true`; every surface asks `networkEnforcedFor(target)`. + The shared `set_channel_network` tool description also explicitly distinguishes ordinary + proxy-mode enforcement from legacy bridge/raw-socket policy and `/sudo` direct-host policy, + which has no OS-level egress cutoff. This description does not change runtime enforcement. → TEST-PLAN: Sandbox boundaries; Egress proxy, placeholders and `--network none`. - **Retired 2026-09-03 (Linux + containers only):** nothing replaces it — with no host sandbox there is no user namespace for AppArmor to restrict, and rootless Podman brings its own uid mapping (`--userns=keep-id`, `/etc/subuid`). **Linux hosts keep their Bash sandbox under Ubuntu's AppArmor userns restriction**: Ubuntu 23.10+ @@ -3463,6 +3932,14 @@ are retired, bullet by bullet; everything else stands. a gateway run. Closing the window removes the live token and lease; the wrapper itself is inert. The lease record itself lives in daemon-owned metadata under the gateway root, never in the agent-writable artifact directory. → TEST-PLAN: Container runtime (v0.8 P1). +- **SSH uses the developer's current-role history scope:** organization admins connect to the + admin container/HOME and members or guests to the project container/HOME, matching ordinary + chat turns by that person. The broker rechecks rank on refresh and disconnects on scope + changes before preparing credentials. Per-user session files, held MCP relays and last-session + login cleanup stay within the selected container. The existing Admin + operator-home SSH + refusal stays enforced, with a second check on the resolved target. Connection guidance names + the requester's scope; no history migration or deletion is performed. + → TEST-PLAN: SSH runtime scope and history parity. - **Developers SSH into a channel container, not into the host** (`docs/SSH-ACCESS.md`). A person registers ONE public key once from chat (`add_my_ssh_key`, bound to the identity that pasted it, fingerprinted like `ssh-keygen -lf`, private keys and DSA refused, RSA under 2048 bits refused); @@ -3825,7 +4302,14 @@ are retired, bullet by bullet; everything else stands. configuration (…_ID, _REPO, _ORG, _REGION, _USER, _URL…), or its value is a URL or a file path; otherwise HIDDEN — STRIPE_SECRET_KEY, API_SECRET and *_CLIENT_SECRET included. A readable name is injected raw and flagged `unprotected` — or withheld under `containerEgressSecretsStrict` - ("Withhold readable variables"). A hidden one gets a placeholder behind an **approval rule**: a + ("Withhold automatically readable variables"). An explicit stored `exposure: readable` choice + permits raw injection even in strict mode, only for the winning scope's entry. Automatic or + hidden password classification does not grant this exception. `list_secrets` separates explicit + readable entries from automatic withholding and respects conversation → personal → organization + precedence in its summary; listings never reveal values. Readable output follows the existing + visibility policy. Returning to hidden/auto takes effect at the + next spawn. Existing scope authorization and readable approval cards still apply. + → TEST-PLAN: Explicit readable secrets in strict mode. A hidden one gets a placeholder behind an **approval rule**: a credential-like header or query parameter (the known auth headers, or a name with auth, token, key, secret, session, signature…), the bearer/raw/Basic positions, and only the servers an admin approved for that secret (`approvedHosts` on its entry, kept across a rotation). Presented to any @@ -3941,8 +4425,9 @@ are retired, bullet by bullet; everything else stands. this release (read before the first-boot password is saved), so an upgrade never silently withholds a secret a working channel uses; a stored choice is never changed. `list_secrets` ends with a **Finding** line naming every listed secret that has no egress rule and what happens to it - (withheld under strict, raw otherwise, raw today under the legacy bridge) and how to declare its - hosts; the settings UI says the same. → TEST-PLAN: Codex login relay, strict secrets, static check + (withheld under strict unless explicitly made readable, raw otherwise, raw today under the legacy + bridge) and how to declare its hosts; explicit readable entries are reported separately and the + settings UI explains the exception. → TEST-PLAN: Codex login relay, strict secrets, static check (container-secrets P4). - **A static check keeps secrets out of the artifact dir (container-secrets P4).** `npm run check:static` runs `scripts/static-secret-writes.mjs` over `src/`: a `writeFile*` / @@ -3998,7 +4483,9 @@ are retired, bullet by bullet; everything else stands. attach and no exit code to read from the child. Output goes to a log under the artifact mount — the same absolute path on both sides, so the daemon tails the very file the job writes — and the wrapper appends a `[cg-exit:N]` marker as its last act, so a finished job reports a real exit code - (a missing marker is "unknown", never 0). The persisted row records `{backend, runId, container}`, + in the structured completion audit (a missing marker is "unknown", never 0). Completion notices + and engine continuations retain their plain-language outcome. The persisted row records + `{backend, runId, container}`, so after a daemon restart a recovered job is probed and signalled BY ITS RUN ID through the backend, never by the stale pid of a client process that is long gone. The memory reviewer follows the same rule: it resolves its own target, takes a `review` lease, and runs in the channel's @@ -4241,6 +4728,7 @@ are retired, bullet by bullet; everything else stands. from the server-filtered table so conversation access editors never inherit a search subset; out-of-order responses are ignored and filtering out the selected row closes its drawer. Skills counts match saved personal grants, including unavailable saved skills, and refresh after Save. + Personal email and secret checkboxes retain their natural width so their labels fit the drawer. → TEST-PLAN: Admin UI (redesign). - Channel **Instructions** sub-tab: edit the channel's `CLAUDE.md` from the admin UI. Stored as a per-channel instruction override (`meta.instructions`) that the folder provisioner regenerates @@ -4733,10 +5221,13 @@ are retired, bullet by bullet; everything else stands. air-gapped promise true: an offline payload is stamped as verified at read time, so an expired one routed through grace would have looked freshly checked forever. A response whose signature does not verify changes nothing in either direction. An in-process event fires on every state change. -- **Two enforcement points**, both in `licenseAdmission()` (`src/ee/limits.js`), called from the +- **Monthly enforcement**, in `licenseAdmission()` (`src/ee/limits.js`), called from the run orchestrator before a turn provisions a folder, mints a session, or spawns an engine: - conversation admission (the month's first N distinct conversations are the allowed set, - persisted in `license_usage`) and the per-conversation monthly cap, counted AT SPAWN for every + legacy active-channel admission and channel AI-run counts (persisted in `license_usage`), plus + 100 active chat threads per channel per UTC month (`conversationsPerChannelPerMonth`). + Existing threads retain admission through a stricter limit or downgrade for that month; new + threads reset with the next UTC month. Thread activity starts tracking on upgrade, without + inventing historical usage. AI runs are counted AT SPAWN for every origin except memory-review runs, with a one-time 80 % warning prefixed to the answer the user is already getting. A refused turn returns a short, platform-degraded reply carrying the sign-up link — never an exception, never silence, and no run starts. @@ -4835,6 +5326,23 @@ are retired, bullet by bullet; everything else stands. beyond the last batch's cutoff (backup first; a batch is recorded even on zero matches so settled history is never rescanned) — updating + restarting another gateway recalculates its history with no manual command. +- **Claude turns are charged their own delta, whole tree, per model.** Claude Code's terminal + result reports the provider SESSION's cumulative `total_cost_usd` and `modelUsage` (CLI ≥ + 2.1.277 restores a resumed session's earlier spend) while `usage` covers the main loop only, + without subagents. The ledger (`src/db/claude-session-usage.js`, applied inside `recordUsage` + for every call site — interactive, warm pool, scheduled, background, API, memory review) + differences each run against the same session's last recorded cumulative, so a resumed thread + is never charged its history again; records one `usage_components` row per model the run spent + on (the answering model as `root`, a Sonnet subagent or the Haiku helper as `auxiliary`) with + the CLI's own per-model cost (`pricing_basis = claude-cli-reported`), so the Models chart and + the per-model split show subagent spend under the model that incurred it; and keeps the raw + cumulative figures beside the row (`provider_cost_cumulative`, `provider_model_usage`) as the + next run's baseline. A cumulative that goes DOWN (a CLI that restarted its totals) is taken at + face value and never recorded negative; a per-model split that does not reconcile with the + run's cost delta is dropped as a whole rather than charted inconsistently. Migration 41 + re-differences the Claude rows recorded since the image first pinned a cumulative-reporting CLI + (2026-09-24) losslessly: each row's original figure survives in `provider_cost_cumulative`, and + the repair is idempotent. → TEST-PLAN: Per-model dashboard stacking. - **Per-model Codex Standard API-equivalent rates** (Settings → Behavior): Codex reports no dollar cost, so the ledger and reply footer estimate attribution value from an editable $/1M table — input / cached-input / output per model (GPT-6 Astra/Sol/Luna, GPT-6.1 Sol, GPT-5.6 @@ -5166,12 +5674,8 @@ are retired, bullet by bullet; everything else stands. mirror remains current. Bundled source skills live in `src/gateway/skills/bundled/`. - Skills admin saves refresh existing workspaces immediately. Boot and a five-second daemon reconciliation pass refresh changed catalog/template/grant state, including MCP changes. Failed - writes are reported and retried; conflicting selections for a shared folder are reported. -- The Admin UI makes shared-folder assignments visible before they break a run. Every channel or - DM whose effective working folder is assigned to another conversation gets a red warning row in - Conversations. The working-folder browser shows a red, named warning whenever the directory being - viewed is already assigned elsewhere, before **Use this folder** can create another shared - assignment. Both views use the runtime's resolved path logic rather than raw string comparison. + writes are reported and retried; shared folders follow one linked owner setup. +- The Admin UI shows shared-folder conversations with a linked marker. The folder picker names existing assignments and explains inheritance; the editor opens the source conversation and locks inherited settings. Both views use the runtime's resolved path logic rather than raw strings. - Runtime settings provide **Reset to default** beside **Browse**. It clears only the custom working folder through the normal Save/Discard flow; the default remains `~/ChannelGate///`. Existing files stay in their original location. @@ -5257,3 +5761,41 @@ schemas, selected identities/accounts, independent multi-execute batches, persis workbench scripts, pagination and resumable checkpoints. Local file staging, protected credentials and operator overrides retain their existing rules. This is agent guidance, not a local Composio sandbox or a new runtime tool. + +- Slack bot removal archives the registered channel, displays Archived in the conversation list/detail, blocks admin changes and new runtime launches, and skips skill/configuration and Drive sync propagation. Re-inviting the bot resumes synchronization without deleting workspace files or sessions. Shared folders can still change through another active conversation that uses the same folder. +- Free cost history exposes the last 30 days in audit, summaries and dashboard (gateway and external usage); Enterprise exposes all history. Stored accounting and monthly admission counters remain intact. Verify both engines at the 30-day boundary and after upgrade/downgrade. + +- Free cost-history visibility applies to structured costs in Audit events, HTTP run-job views and schedule telemetry as well as the ledger/dashboard. Operational records and answer text remain available; stored accounting is not deleted. + +- Conversation admission identity stays separate from engine-session identity: background/nested agents, known scheduled delivery threads and scheduled question continuations share the existing parent slot. An automation fire without a chat anchor uses its own stable logical conversation identity. +- Slack archival survives delayed join delivery and concurrent access/runtime resets. Join restoration requires current membership; whole-record settings saves preserve lifecycle admission state. At startup and Socket Mode reconnect, configuration propagation and new runtime launches wait for a complete calling-bot membership snapshot. Missing removal/rejoin events are reconciled from all paginated public/private/group-DM memberships, with periodic reconciliation and retry on incomplete/error responses. A membership API failure pauses verification without falsely archiving channels. + +## Slack Connect streaming and terminal session cleanup (2026-11-03) + +Streaming uses the requesting user's workspace (`user_team`, otherwise a verified cached user lookup) rather than the bot installation's workspace. A completed reply reasserts native `active` after stream finalization or classic fallback. Compatibility status clears precede native completion, and session writes are serialized with producer ownership checks so a delayed clear cannot override the next turn. + +## Rootless Podman in system services (2026-11-05) + +System-service installs add a managed per-account `cgroupfs` configuration without replacing operator settings, verify the effective manager, and bind persistent private temporary directories into the unit. The daemon's restart no longer deletes the backing paths retained by a rootless pause namespace. Limits probes use no network and never pull images; actual kernel memory/CPU/pids values must match before support is advertised. Silently ignored flags, missing images, namespace failures, timeouts and generic launch errors are distinguished from delegation failures. Existing-store recovery is an explicit idle operator procedure; startup never globally migrates or prunes containers. + + +### Codex process creation resource failures + +Codex's own router diagnostic for a failed unified tool-process creation starts a 15-second +recovery window. A successful command or completed turn cancels it, including timestamped older +stderr delivered after successful stdout. Persistent failure ends with `ENGINE_RESOURCE_EXHAUSTED` +and an actionable process/thread-limit message, retains partial usage, and is never automatically +replayed on another engine. Generic EAGAIN or tool output does not trigger this path. + +A best-effort runtime-local snapshot records process/thread counts, process soft/hard limits and +leaf cgroup PID current/max/event counts. Leaf counts do not establish effective ancestor limits. +The probe never reconfigures/recreates the runtime, retries, reads argv/environment/credentials or +extends the recovery deadline; unavailable probes remain explicitly unavailable. Bounded snapshot +facts are retained with the run-error event. No container image rebuild is needed for this reducer. + +## Node 24 runtime baseline + +The daemon, installer, transactional updater and documentation build require Node 24.0.0 or +newer. The runtime image and release workflow use Node 24; CI exercises both the exact minimum +and the current 24.x release. `.nvmrc` selects the Node 24 development line. Existing operators +upgrade host Node before updating; rebuilding the image alone does not upgrade the daemon. diff --git a/INSTALL.md b/INSTALL.md index 94a8828b..e9455f3c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,11 +5,14 @@ A step-by-step guide to get the gateway running on a fresh Linux host. ChannelGa Podman container (Ubuntu 24.04 is the tested distribution). For what it does and how it works, see [README.md](./README.md). +Before choosing a server, see [VPS sizing and hosting](documentation/src/content/docs/configuration/vps-sizing.md) +for production and pilot configurations, concurrency, storage, and backups. + ## 1. Prerequisites - **Linux with systemd** (Ubuntu 24.04 tested). The daemon refuses to start anywhere else. -- **Node.js ≥ 22.13** — `node -v` (the gateway's SQLite store uses the built-in `node:sqlite`, - stable from 22.13). Install from or your distribution's packages. +- **Node.js ≥ 24.0.0** — install the current Node 24 LTS release and verify with `node -v`. + The daemon and default runtime image use Node 24. Install from or your distribution's packages. - **Claude Code CLI**, installed and signed in on the host as the daemon user. Turns run inside the channel containers (the runtime image ships the engine CLIs), and the gateway relays that host login's short-lived access token into each container — the credentials file itself is @@ -90,7 +93,11 @@ builds/probes the image in that user's own rootless Podman store. A foreground ` setup installs `rclone` into `~/.local/bin` instead. Downloads are checked against the release checksum; a failed optional install leaves Drive sync dormant and prints a manual remedy. It writes `/etc/systemd/system/channelgate.service`, with `ProtectSystem=strict`, `ProtectHome=read-only` -and private `/tmp`. The daemon must permit Podman's setuid namespace helpers; every channel +and persistent private `/tmp`/`/var/tmp` backing under the service HOME. A managed Podman +configuration keeps container cgroups inside the delegated system service. Existing installations +need the idle migration checks in `docs/OPERATIONS.md` (Rootless Podman system-service upgrades); +installing the new unit alone does not repair old containers or a stale pause namespace. +The daemon must permit Podman's setuid namespace helpers; every channel container still uses no-new-privileges and dropped capabilities. API credentials belong in the 0600 `/var/lib/channelgate/service.env` before starting real turns. Manage it with `systemctl`: @@ -230,8 +237,10 @@ For a shared workflow, start with one dedicated channel and one representative t history in the admin UI when reviewing the pilot. Slack Socket Mode needs no public inbound URL. If operators need the admin UI or approval links -from outside the host, put a tunnel or reverse proxy in front of the loopback service and set -**Settings → Connection → Public URL** to its HTTPS origin. See +from outside the host, put a tunnel or reverse proxy in front of the loopback service, set +**Settings → Connection → Public URL** to its HTTPS origin, and set **Behind a reverse proxy** to +the proxy you run (`cloudflare`, or `xff` for nginx/caddy) so the login backoff keys on each real +client — without it, all remote logins share one backoff bucket. See [reverse proxies and tunnels](./docs/OPERATIONS.md#reverse-proxies-and-tunnels). ## Backup & restore config (encrypted) diff --git a/README.md b/README.md index f6513e3b..acfa733f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![License: Sustainable Use License 1.4](https://img.shields.io/badge/license-Sustainable%20Use%20License%201.4-2f6f4e)](./LICENSE.md) [![CI](https://github.com/makeitfutureDev/channelgate/actions/workflows/ci.yml/badge.svg)](https://github.com/makeitfutureDev/channelgate/actions/workflows/ci.yml) -[![Node.js minimum version](https://img.shields.io/badge/node-%E2%89%A5%2022.13-3c873a)](./docs/COMPATIBILITY.md) +[![Node.js minimum version](https://img.shields.io/badge/node-%E2%89%A5%2024.0.0-3c873a)](./docs/COMPATIBILITY.md) [![Chat platforms and support status](https://img.shields.io/badge/platforms-Slack%20%C2%B7%20Teams%20%C2%B7%20Google%20Chat-4a154b)](#supported-platforms-and-ai-engines) [![AI engines and support status](https://img.shields.io/badge/engines-Claude%20Code%20%C2%B7%20Codex%20%C2%B7%20OpenCode-8a3ffc)](#supported-platforms-and-ai-engines) @@ -40,7 +40,7 @@ Formerly Claude Gateway for Slack. ## Getting started -**Requirements:** Linux, Node.js ≥ 22.13, rootless Podman, and credentials for the engine you will +**Requirements:** Linux, Node.js ≥ 24.0.0, rootless Podman, and credentials for the engine you will use. The default Claude setup uses the daemon user's host login, a configured setup token or `ANTHROPIC_API_KEY`; Codex requires its own authentication. See the [installation guide](./INSTALL.md) for prerequisites and service-account setup. @@ -56,8 +56,12 @@ The installer checks prerequisites, installs dependencies, scaffolds `.env` and container image with the engine CLIs and media tools. If the image build fails, fix the reported prerequisite and run `npm run build:image` before sending the first prompt. -1. Open the admin UI at . On a new install, use the admin password printed - once at first boot, or your configured password. The UI starts without Slack credentials. +1. Open the admin UI at . First-time setup uses username `admin` and the + password printed once at first boot (or your configured password), then asks you to create a + passkey. Set the HTTPS Public URL before enrolling keys for remote access. After first + enrollment, select **Sign in with a passkey** and choose your saved account; password login is + retired and no username entry is needed. The UI starts without + Slack credentials. See [admin access and recovery](./docs/OPERATIONS.md#admin-accounts-passkeys-and-recovery). 2. Create a Slack app at using the bundled [Slack app manifest](./slack-app-manifest.json). Generate an app-level token with `connections:write`, install the app, and copy its bot token and signing secret. @@ -142,8 +146,8 @@ catalog, including edge cases and links to regression coverage. - **Channel environment secrets:** configure credentials for tools such as provider CLIs through write-only settings. Names are validated, rotations retire warm sessions, and known values are redacted from replies and job output. Runtime access and limits are documented below. -- **Protected administration:** password sign-in, masked secret listings, password-confirmed - reveal for eligible settings, audit records and separately scoped API access. +- **Protected administration:** individual usernames and passkey sign-in, masked secret listings, + recent-passkey verification for eligible secret reveals, audit records and separately scoped API access. ### MCP integrations and Composio accounts @@ -349,20 +353,24 @@ license. See [why teams choose a self-hosted agent gateway](./docs/WHY.md) for m noncommercial use. It is not OSI open-source software, and no version is relicensed automatically. -| | Registered channels | Registered users | AI messages per conversation per month | -| --- | --- | --- | --- | -| **No key** — install and run | 20 (one active per month) | 20 | 500 | -| **Registered (free key)** — an account with your email | 20 | 20 | 500 | -| **Enterprise key** — agreement with Makeitfuture | unlimited by default | unlimited by default | unlimited by default | - -A free key lifts the one-active-conversation limit. Registered installations support up to 20 -channels (including DMs) and 20 users, at 500 AI messages per conversation per month. Enterprise -removes these default caps and adds per-channel approved harnesses. A key belongs to the -organization that operates the deployment; core confinement remains available in every tier. +| | Registered channels | Registered users | Active conversations per channel per UTC month | AI runs per channel per UTC month | +| --- | --- | --- | --- | --- | +| **No key** — install and run | 10 (one active per month) | 10 | 100 in the active channel | 500 | +| **Registered (free key)** — an account with your email | 10 | 10 | 100 in each channel | 500 | +| **Enterprise key** — agreement with Makeitfuture | unlimited by default | unlimited by default | unlimited by default | unlimited by default | + +A conversation here is a chat thread, distinct from its registered channel. A free key supports +up to 10 channels (including DMs) and 10 users, with up to 100 active conversations **in each +channel per UTC month**, 500 AI runs per channel per month, and one month (30 days) of +cost-tracking history. Repeated messages in an admitted thread do not consume another +conversation slot. Thread counting starts with activity after the upgrade; old threads are +counted when next used. Enterprise removes these default caps and adds per-channel approved +harnesses. A key belongs to the organization that operates the deployment; core confinement +remains available in every tier. Definitions, the offline grace period, and exactly what a deployment reports are in [`docs/LICENSE-KEYS.md`](./docs/LICENSE-KEYS.md). -- **Registered — run it yourself.** Up to 20 registered channels and 20 users with a free key, on your own +- **Registered — run it yourself.** Up to 10 registered channels and 10 users with a free key, on your own infrastructure. Modify SUL-covered code within the license; the EE directory has separate contribution and redistribution conditions. There is no obligation to publish internal modifications. - **Partner — operate it for clients.** Installing, configuring, supporting or running a **dedicated diff --git a/SECURITY.md b/SECURITY.md index 09479d0c..a2a8eb74 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,11 +24,11 @@ with the production audit command above and record any accepted exception in thi | Advisory | Severity | Applicability | Owner | Next review | |---|---|---|---|---| -| None | — | The committed production lockfile has no reported vulnerabilities in the 2026-09-07 npm audit. Re-run the audit for each candidate; this is not a future guarantee. | Gateway maintainers | Next dependency update | +| None | — | The committed production lockfile has no reported vulnerabilities in the 2026-11-06 npm audit. Re-run the audit for each candidate; this is not a future guarantee. | Gateway maintainers | Next dependency update | -`@composio/core` remains at `0.14.0`: the proposed `0.18.0` requires Node >=22.22.3, -above this project's supported Node 22.13 floor. Review that update independently with a runtime -compatibility decision; a green test on Node 24 alone is insufficient. +The supported runtime is Node 24.0.0 or newer. The current Composio SDK and other locked +dependencies are validated against this baseline; package engine declarations are checked +separately from exercised API compatibility. Re-run the production audit for every candidate. ## Reporting diff --git a/SUPPORT.md b/SUPPORT.md index 166fff64..f0ce847a 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -32,7 +32,7 @@ A report we can reproduce gets fixed; one we cannot gets questions. Include: `version` from `package.json`. 2. **Operating system** — the Linux distribution and version, and how the daemon runs (foreground, systemd system unit, systemd user unit). -3. **Node version** — `node --version` (the floor is 22.13). +3. **Node version** — `node --version` (the floor is 24.0.0). 4. **Engine** — Claude Code or Codex, and the CLI version (`claude --version` / `codex --version`). 5. **Chat platform** — Slack, Google Chat, or Microsoft Teams, and whether it happened in a DM, a channel, or a thread. diff --git a/TEST-PLAN.md b/TEST-PLAN.md index 4b8b256c..82df9c07 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -1,5 +1,188 @@ # ChannelGate — Test Plan +## Shell tasks versus subagent progress (2026-11-06) + +Run `node --test test/progress-report.test.js test/slack-progress.test.js test/codex-message-to-reply-e2e.test.js`. +Feed the actual Claude parser foreground/background `local_bash` starts followed by untyped +progress, updates, notifications and late tool-use aliases. No frame may become agent activity; +ordinary Bash tool use/results remain visible. Interleave real `local_agent`/`remote_agent` +lifecycle frames and retain their stable identities, elapsed tool progress and terminal states. +Retain Agent/legacy Task launches and older agent messages without optional task-type metadata. +Through the actual Slack progress consumer, shell work stays Using tools and genuine agents +produce Coordinating agents; Codex command execution remains a negative control. + +Live acceptance on an activated candidate must observe a harmless foreground shell wait and +genuine in-turn subagent work separately in each engine, with exact native status/card evidence, +tool traces and final delivery. The Claude-only background-task protocol variant is isolated; +do not start harness background work in production solely for this check. The confirmed parser +defect follows the official SDK task-type contract; a prior live Bash-only misleading label is +corroborating presentation evidence, not proof of its unretained raw event shape or loaded +revision. Isolated parser/status tests do not establish live Slack rendering or deployed acceptance. + +## Readable background continuation errors (2026-11-06) + +`node --test test/background-continuation-errors.test.js test/process-outcome.test.js` +executes BackgroundJobs through runMessage and the real Claude/Codex parsers, with a spawned +fixture CLI emitting each engine's nested provider-error event. Assert readable notices without +JSON, secret redaction in notices and audit, no partial secret at the 400-character boundary, +redaction after JSON escape decoding, preserved provider facts and diagnostic codes, +released runtime leases, and no second spawn when delivery resumes after the failed continuation. +The runtime backend and Slack delivery boundary are fixtures, not live provider/client acceptance. +Private REL-RUNTIME-07 remains the per-engine acceptance gate: reproduce the nested provider +failure in its isolated acceptance setup, inspect the actual continuation notice and durable +recovery, and keep deployed/native observation distinct from these automated results. + +## Automatic continuation after variable server approval + +Automated: `node --test test/secret-host-continuation.test.js test/egress-secret-approval.test.js test/durable-approvals.test.js test/approval-links.test.js`. +Live SECRET-HOST-CONTINUE-01: run separately in Claude and Codex worker fixtures with network ON, +Lean OFF, an approved requester and a disposable hidden HTTPS token for an operator-controlled +public endpoint with no approved hosts. Ask to check the endpoint without printing the token. +After secret-refused, click Allow on server without sending continue. Require one automatic retry +and result in the original thread/session as the original requester, including a different admin +approving an organization token. Repeat with a personal owner approval, while the source turn is +active, after it ends, and with a pending card across restart. Require serialized runs and usage +for every continuation. Duplicate clicks, denial, stale/rotated tokens, unauthorized approval, +revoked requester access and Stop while queued must cause no additional execution. Queued wakeups +survive restart; an interrupted running wakeup produces a notice without replaying unknown effects. +Never expose token values. Live engine acceptance remains separate from automated regression. + +## Network tool description and direct-host policy + +Run `node --test test/channel-policy-audit.test.js test/network-policy.test.js +test/runtime-access-facts.test.js`. The registered `set_channel_network` description must distinguish +ordinary proxy-mode enforcement from advisory legacy bridge/raw sockets and `/sudo` direct-host +execution with no OS-level egress cutoff. Preserve public internet without a domain allowlist, +OFF access to engine endpoints and selected connectors, proxy denial of private/loopback/metadata +addresses, and no grant of host credentials or mounts. The schema regression does not exercise +or change network enforcement. Live MCP-DESCRIPTIONS-01 acceptance remains separate for Claude +and Codex: inspect the actual delivered tool description and resolved runtime facts without +changing the fixture's network setting or entering `/sudo`. + +## Documentation favicon + +Run `cd documentation && npm run build && npm test`. The output validator requires the +`/docs/favicon.svg` link on every handbook page and a valid SVG asset in the build. In the exported +website fixture, require HTTP 200 with SVG content for that URL and repeat desktop/mobile handbook +navigation, search, themes and menu checks with zero console errors. This website asset is +engine-independent; source checks alone do not replace browser acceptance. + +## Enterprise organization policy acceptance + +Cases EE-POLICY-01 and EE-POLICY-02: for both Claude and Codex, on a disposable Enterprise fixture with Lean mode off set organization instructions and a mandatory skill; verify both appear in a fresh channel run. Downgrade the fixture to Registered and verify the saved policies still apply, while new mandatory grants and changed nonempty organization instructions are rejected with Enterprise guidance. Verify the same rejection via admin UI, HTTP governance/settings routes, MCP org-grant/governance tools and raw settings saves. Mixed governance requests must not partially enable or expose a skill. Explicitly remove the grant and clear instructions; verify subsequent runs omit only those policies and preserve core safety/channel instructions. Re-upgrade and verify new policy writes work. Automated: `test/enterprise-organization-policy.test.js`; live engine acceptance is separate and must not be inferred from unit results. + +## Official Make.com CLI in every runtime image (spec 1.6.3) + +- Automated: `node --test test/container-image.test.js test/cli-catalog.test.js` verifies the + exact Make package pin, build argument, root-owned install before `USER agent`, default setup + image build, official environment names, channel HOME credential location, Make-only API-key + swap rule, legacy API-token compatibility and unmodified routing-zone classification. +- Image acceptance (engine-independent, pending until candidate build): build the candidate + with `npm run build:image -- --tag `. As the non-root image user run + `make-cli --version` and `bash -lc 'make-cli --version'`; both must match the exact package pin. + Resolve the executable and require its package files and global prefix to be root-owned and + unwritable by the channel user. Confirm no Make saved login exists in a fresh HOME. +- Live Claude and Codex (pending): use separate disposable proxy-mode fixture channels, each + with its own least-privilege Make `MAKE_API_KEY` kept Hidden/default and only its correct + `MAKE_ZONE` explicitly Readable, + plus an owned team ID. Ask: "Use the installed make-cli to report its version and list scenario + IDs/names for team ; do not change anything or print credentials." Require the + pinned executable in both engines, correct account/team result and no exposed key. Confirm + Make API requests receive authorized data through the proxy while the API key in the container + remains a placeholder. Repeat after image recreate to verify the default CLI remains available. + Missing credentials or unavailable Make fixtures are blocked live acceptance, never a pass. + + +## Monthly channel USD budget management + +- Automated: `node --test test/channel-budget.test.js test/channel-spending.test.js test/memory-review.test.js test/license-run-gate.test.js`. Covers unlimited/positive-limit validation, invalid/atomic saves, audit, unrelated-save preservation, canonical totals, UTC month/year boundaries, channel isolation, all task kinds, both engines/all origins, memory-review skip, Enterprise entitlement preservation and queued-policy recheck without an engine spawn. +- Browser: `PLAYWRIGHT_BROWSERS_PATH=/opt/channelgate/browsers CG_BROWSER_MODULE=/usr/local/lib/node_modules/playwright/index.mjs node --test test/channel-budget-browser.test.js`; decimal and blank values must persist after reload at desktop and 390px width, with no browser errors. +- Live Claude and Codex (pending): in a disposable Enterprise channel, admin UI → Runtime → Monthly spending limit, save/reopen a positive amount; blank restores unlimited. With canonical recorded current UTC-month spend at the cap, send `Reply with BUDGET-ACCEPTANCE`; require the limit notice and no engine process. Raise/remove the cap and require admission. A non-admin channel manager has no budget setting control or authorized admin API route. +- Isolated fixtures: count canonical root/components across memory and background tasks, exclude other channels/months; unpriced rows must fail closed until repaired. Cross the fixture clock's UTC month boundary and require automatic admission without resetting metadata. Hold one run slot, queue a request, exhaust its cap, release, and require no engine spawn; repeat schedule/API/background and fallback/retry paths. +- Admit a run below its cap and let it finish above: retain its recorded cost, finish normally, refuse the next run/review. Concurrent active runs may overshoot. External provider/service charges are excluded. Private BUDGET-01 through BUDGET-04 definitions exist; live execution remains pending and must use the final positive-cap/unpriced-fail-closed contract. + +## SSH engine status bars (SSH-STATUS-01) + +- Automated: `node --test test/ssh-statusline.test.js test/ssh-session.test.js`. Verify the actual staged Claude command renders, retains lockdown settings and private file permissions, Codex overrides include native footer fields, current-context/cache calculations, live model/effort changes, missing/zero values, warning colours, malformed JSON and terminal control sanitization. +- Live Claude: use `cg-testing-claude-auto`, an approved SSH developer key, a current candidate daemon and a freshly reconnected shell. Start `claude`, accept folder trust, send `Reply with OK`, then change model/effort with the native picker and send again. Require model and effort to follow the selection; context percentage/window match `/context`; folder/branch match the checked-out fixture; available cost/quota fields render without login data or credentials. Before the first response or after compacting, missing context must stay unknown. Resize the terminal and check legibility and green/amber/red thresholds using the staged formatter with owned JSON fixtures. +- Live Codex: repeat in `cg-testing-codex-auto` with `codex`; require native footer model plus reasoning and used-context percent to agree with `/status`, and folder/branch/quotas when available. Change model/effort, resume, and verify the footer updates; launch with an explicit `-c tui.status_line` override and verify the user's CLI override wins. No changes to MCP, login, permission or network policy. +- These are dual-engine SSH fixture cases; automated checks are not terminal-rendering/live acceptance. Live SSH visual acceptance remains pending until executed against the deployed candidate. New sessions load daemon changes after its next safe restart; no container image rebuild. + +## Skill ownership terminology (2026-11-04) + +- Wording-only, engine-independent: open Skills with a gateway-owned fixture (`ownerKind: "local"`) and a Git-source fixture. Require **Gateway-owned** in the catalog owner column/filter and creation form, and **Gateway-owned skills** in the assignment picker. Filter/select/save the gateway-owned fixture; API owner/filter values must remain `local` and the grant must persist. Repository-backed labels must remain distinct. +- MCP: `list_skills` and `get_skill_info` on the same gateway-owned fixture must describe gateway ownership; catalog create/update/publish tool descriptions must use the same vocabulary. Existing skill CRUD, publishing and scope regression suites verify unchanged contracts. +- Passed: 31 focused skill CRUD, publishing, admin UI and picker regression checks; static validation. Disposable Chromium acceptance passed for catalog owner/filter/create labels, distinct Git-source label, raw `owner=local` filter request, picker source label/filter/add and unchanged stored ownership. This wording-only check is engine-independent. +- Existing full conversation/template browser suite: compact layout case passed; conversation navigation case timed out before reaching the skill picker (`#channel-detail .subtab[data-pane="skills"]`). Its unrelated full-flow acceptance remains unresolved. + +## Slack email for password and code sign-in (2026-11-05) + +- Local automated verification (2026-11-05): 4,246 passed / 40 opt-in skipped / zero failed in + the full coverage suite at concurrency 4; coverage 94.09% lines, 86.63% branches, 90.42% + functions. All 16 combined Chromium checks passed. Static (994 JavaScript files), secret scan, + security coverage, DCO and root high-threshold dependency audit passed. Documentation build + verified 153 pages, 27,783 internal links and all 130 gateway controls. +- Engine-independent: the HTTP/browser authentication path never launches an engine. Use scratch + databases, disposable passwords and simulated Slack lookup/private delivery only. +- Verify whitespace/case normalization, exact provider email and stable user ID matching, current + Admin status, password-before-lookup, no account directory disclosure, missing-scope guidance, + and refusal of absent/deactivated/bot/mismatched/non-admin users. Editable user-store emails + must not resolve identity. Preserve OTP browser binding, expiry, five guesses and one-time use. +- Reauthentication uses the current linked session and requires password plus its private Slack + code; session/role/password/settings changes while lookup, delivery or verification is pending + must not restore stale authority. Existing passkey account labels and credentials are unchanged. +- Live acceptance pending: on a disposable gateway grant `users:read.email` and reinstall the + Slack app, enable optional password/Slack-code sign-in, then sign out. Enter the existing Slack + administrator's email and dashboard password; require a private Slack code to that same member + and a full session linked to the original stable user ID. Test reauthentication and a non-admin + email. Record only success/failure, message links and redacted screenshots. Check scope failure + guidance on a separate test app lacking email access; never change the production bot to test. +- Private QA definitions recorded through the established personal QA connection: + `ADM-SLACK-EMAIL-01` (email sign-in/provider checks) and `ADM-SLACK-EMAIL-02` + (authority races/session-bound reauthentication). Both are engine-independent and explicitly + retain human deployed acceptance as pending; no simulated delivery is recorded as live PASS. + +## DM template egress and HTML attachments (2026-11-05) + +- Automated: `node --test test/egress-service.test.js test/skills-templates-assign.test.js test/skills-workspace-sync.test.js test/new-channel-template.test.js test/slack-download-file.test.js test/managed-write-symlinks.test.js`. Real CONNECT/TLS tests cover user/admin template ON over stored OFF, live template OFF over stored ON, live re-enable on the same listener and custom-DM independence. Download tests cover HTML and identical renamed text, ordinary HTML containing Slack/sign-in prose, Slack authentication redirects and split-chunk branded error pages, real HTTP errors, byte caps, and no-follow writes. +- Claude and Codex live acceptance (`DM-EGRESS-TEMPLATE-01`): use Apps DM / Tiberiu DM on an isolated QA deployment, organization user then admin DM template, Worker + network ON; preserve prior settings. With saved DM network OFF and inherited ON, ask each engine: "Fetch https://www.google.com and report only the HTTP status." Require a real public response through the DM proxy. Flip only the assigned template OFF, repeat, and require proxy `network-off`; turn it back ON and require the next request to succeed without recreating the container. Repeat custom-DM ON/OFF and a linked-folder DM to ensure the source policy wins. Restore fixture settings; no disabling TLS or private-address checks. +- Claude and Codex live acceptance (`SLK-HTML-FILE-01`): in the dedicated Attachment Fixture on `cg-testing-claude-bash` / `cg-testing-codex-bash`, upload a harmless HTML file containing marker HTML_ATTACHMENT_OK and the same bytes as `.txt`; ask "Read both attachments and report the marker and SHA-256 of each." Both local files must contain the exact uploaded bytes and equal digests, and no scope warning may appear. In an isolated HTTP fixture return Slack sign-in/error HTML or a Slack authentication landing URL: require a clear file-access refusal and no delivered marker/file. Ordinary documents mentioning Slack sign-in remain valid. UI rendering of HTML and customer re-upload are separate pending human checks; automated regressions are not a live engine pass. + + +## Skill publication recovery (2026-11-05) + +- Automated: `node --test test/skills-publication-recovery.test.js test/skills-platform.test.js test/skills-standalone.test.js test/skills-admin-api.test.js` covers successful unpinned updates, failed publication protection, retry through publication or update (including identical bytes), stale source sync, next source activation, manual/proposal preservation, legacy migration and concurrent operator/newer edit decisions. The new metadata is exercised through the registered gateway tools in both Claude and Codex contexts. +- Live Claude and Codex acceptance (pending): use `cg-testing-claude-admin` and `cg-testing-codex-admin`, Contact as admin actor, and a disposable skill `publication-recovery-probe-` in a dedicated writable Git publishing fixture; never change production sources or credentials. Ask: "Update the probe to reply RECOVERY_V2 through update_skill, then report current/effective revisions, publication and pin reason." With the fixture's publisher returning HTTP 500, require the saved V2 to stay effective with `Pin: publication recovery`, including after a source sync delivers V1. Restore publisher success, ask "Retry the exact same update," and require publication success, no new revision, `Pin: none (following current)`, V2 effective and `skill_publication_pin_cleared` audit evidence. Repeat recovery using `publish_skill`, then a changed V3 update. Each must clear only its own recovery pin. +- In the same two fixtures, an admin manually pins the probe (and separately approves a source override); retry publication and require the intentional pin and its reason to remain. Verify library badges distinguish recovery from intentional pins. An existing pre-migration pin must remain manual/legacy. Deactivate/exclude only the disposable probe after testing. Record exact thread, candidate and results in QA cases `SKILL-PUBLISH-RECOVERY-CLAUDE` / `SKILL-PUBLISH-RECOVERY-CODEX`; automated tests and the earlier Atlas investigation are not post-fix live acceptance. + +## Model-specific skills (2026-11-05) + +- Automated: `node --test test/skill-runtime.test.js` covers the `model:` frontmatter reader and engine derivation, the SKILL.md header insertion (after frontmatter, once, catalog bytes untouched, materializer sees it as unchanged), the full decision table (disabled / no preference / pinned / already / inline / second skill → subagent / other engine → delegate / engine unavailable), every tool reply, the live-turn registry keyed by thread, the channel switch on the audit/folder-link/managed-block surfaces, `set_channel_skill_swap` authorization + audit, `use_skill` relaying the live turn's reply, and the orchestrator E2E against the Claude stub: a mid-turn `use_skill` ends the attempt, the continuation RESUMES the session with `--model sonnet`, the footer/result name `sonnet`, the thread keeps `sonnet` on the next message, a `/model` pin wins and the skill is refused as pinned, the runtime reset clears the preference, and the switch off refuses with the current model kept. `channel-settings-modal`, `access-settings`, `mcp-control-plane-approval`, `folders-settings` cover the new tick, flag parsing and tool classification. +- Live Claude acceptance (pending): in `cg-testing-claude-bash`, turn **Allow skill engine and model swapping** on (Settings → Access ticks or `set_channel_skill_swap`), grant a disposable channel skill `swap-probe` whose SKILL.md declares `model: sonnet` and whose body says "reply with the word SWAPPED and the model you are". As Apps, in a thread whose channel default is Opus, ask "use the swap-probe skill". Pass only when the thread shows a `use_skill` tool row, the reply starts with the "Switched this thread to sonnet" note, the footer names sonnet, `run_skill_swap` is in Observability, and a plain follow-up in the same thread also runs on sonnet without a note. Then `/model` → this thread → opus and ask again: no switch, the tool reply says pinned. Turn the switch off and repeat in a new thread: no switch, the reply says swapping is OFF. +- Live Codex acceptance (pending): same fixture in `cg-testing-codex-bash` with `model: gpt-6.1-sol` on a channel defaulting to another Codex model; pass rules identical, with `codex exec resume` carrying the new model. Cross-engine: in the Claude fixture grant a skill declaring `model: gpt-6.1-sol`; pass when the model calls `run_agent_in_background` with `engine: codex, model: gpt-6.1-sol`, the agent report posts back, and the launching thread stays on Claude. +- Authoring (automated, `test/skill-runtime.test.js`): `setSkillModelFrontmatter` sets/replaces/clears/validates the line; `create_skill` with `model` writes it and refuses an unknown model before creating anything; a model-only `update_skill` rewrites the frontmatter, keeps the other files, `""` clears it, and an empty call answers "Nothing to change". Live (engine-independent): in a fixture ask "create a channel skill swap-authored that replies SWAPPED, and make it prefer sonnet" — pass when the created SKILL.md carries `model: sonnet`, the library detail shows "Preferred model: sonnet", and "make swap-authored prefer no particular model" removes the line. +- Teams/Google Chat (automated): `teams-settings-general` saves `skillRuntimeSwap` in a channel and a custom DM without touching Lean, and rejects a malformed value; `googlechat-settings-commands` parses `skillswap on`. Live checks stay on hold with the other Teams/Google Chat fixtures. +- Engine-independent: the admin UI Runtime pane toggle and the Slack tick persist `skillRuntimeSwap`, show in `/status`'s managed block line "Skill model swap", and write a `channel_meta_changed` audit row with that key; a linked folder follows its source's value. + +## Library missing skill manifests (2026-11-04) + +- Regression: `test/skills-admin-api.test.js` verifies missing host folders, path/user attribution, no token exposure, plugin exemption, read-only diagnostics and automatic disappearance after repair. `test/skills-catalog-filters.test.js` covers filter composition. +- Browser: `CG_BROWSER_MODULE= node --test test/skills-folder-issues-browser.test.js` opens a disposable admin app, verifies the default diagnostic row, missing/healthy filters, attribution and absence of governance controls at desktop and 390px width. +- Fixture: Admin UI with disposable `synced/nested/SKILL.md` but no `synced/SKILL.md`, a valid catalog skill, and a user personally granted `synced`. Open Skills → Library and select Skill file status → Missing SKILL.md. Require the warning row and folder/user details; no native plugin false positives. Engine independent because no engine runs or grant writes occur. QA case `SKILL-FOLDER-LIBRARY`; existing dual-engine loader cases remain unchanged. + + +## Channel SSH egress approval (2026-11-04) + +- Automated: `test/egress-hosts.test.js` covers exact public destinations, denied private/malformed hosts, current worker authority, network/mode changes, and atomic append. `test/mcp-control-plane-approval.test.js` exercises real MCP tools in Claude/Codex contexts, pending durable cards in Auto mode, no mutation before approval, member approval and live authorization. Existing durable-approval and approval-link suites cover restart persistence, replay and single-use decisions. +- Live Claude and Codex acceptance (pending): in `cg-testing-claude-bash` and `cg-testing-codex-bash`, use an approved non-admin member and a disposable public SSH fixture with a verified host key and existing test key. Ensure Network on, Worker mode and no existing host entry. Ask “Connect to the test SSH server; request approval if it is not allowed.” Require `allow_channel_ssh_host`, a card naming the host and TCP 22/5432/6543, and no append before the human click. Approve as a second authorized non-admin member; require only that host appended, unrelated settings preserved, and successful SSH via `cg-egress-connect` without `/sudo`. Repeat in Auto mode: still requires a click. +- Deny a fresh destination and verify no append. Request another destination, restart the disposable gateway, then approve the saved card once; replay must not execute twice. Revoke requester/approver access, switch to Read-only or turn Network off before approval: no grant. Public IPv4 and IPv6 literals must create approval cards and tunnel SSH bytes without TLS after approval; IP destinations require no DNS lookup. Private IP, wildcard and URL requests must refuse without posting a card. Remove fixture grants/keys after testing. No live pass is implied by automated results. + +## Invalid skill-folder grants (2026-11-04) + +- Automated: `test/run-grant-isolation.test.js` covers personal/shared parent-folder grants, alone and alongside a valid skill; verifies warnings, source preservation, and exclusion from both Claude plugins and the Codex catalog. +- Live Claude/Codex acceptance: use Apps in `cg-testing-{claude,codex}-bash` with a disposable personal grant `synced` containing a nested skill but no root `SKILL.md`, alongside a valid marker skill. Send “Reply SKILL_FOLDER_OK.” Require a normal answer plus a warning naming `synced` and the missing `SKILL.md`, no nested skill in delivered artifacts, and the valid skill retained. Repeat with only the invalid grant. Restore fixture grants afterward. Live chat acceptance pending; remote artifact verification is separate from a human-visible engine run. + + ## Company domain auto-approval (2026-10-28) - Automated: `node --test test/user-auto-approval.test.js test/user-search.test.js` covers @@ -22,7 +205,537 @@ Repeat authenticated Google Chat/Teams sender fixtures when those transports provide email; Teams Bot Framework events without email must remain manual. No engine-specific output is used for the approval pass rule; downstream engine release smoke remains a separate gate. -- Private QA registry update pending: requesting user's personal Airtable connection is unavailable. +- Beta integration (2026-11-03): 46 focused approval/ingest/user-search/code-channel/MCP-inventory checks passed, plus admin-role-race and personal skill-token regression checks. Static validation passed. The existing beta reply-target and MCP-selection safeguards are retained. Chromium acceptance also passed after adding the beta auth-identity bootstrap endpoint to the disposable UI fixture. +- Private QA definitions and blocked Claude/Codex live runs are recorded. Host-side verification confirmed the deployed Slack bot lacks `users:read.email`; live positive acceptance remains blocked until the scope is granted and the app reinstalled. A scratch main pipeline using the real bot profile correctly denied the missing-email sender before engine execution. +## User-level skill catalog tokens (2026-11-03) + +- [x] Automated: `node scripts/run-tests.mjs test/user-skill-tokens.test.js` (then + `test/skills-standalone.test.js`, `test/skills-admin-api.test.js`, `test/skills-admin-ui.test.js`) + — owner-only mint/list/revoke, API principal and unapproved users refused, the per-person cap; + a user token's reach over the real endpoint (library, admitted channel's section, own personal; + not another's personal, not an admins-only channel's section, not a non-discoverable library + skill until granted), edit rules per tier, creation targets, no `sync`; the token dies and + revives with the owner's approval; an admin's token reaches the library but no one else's + personal skill; a catalog token never lists/reads/exports/edits a personal skill; the bearer + gate runs before the body parser and bad tokens back off with 429. +- [x] `npm run check:static`, `npm run secret-scan`. +- [ ] Live, engine-independent (HTTP): with a catalog token holding `read,sync`, run + `library_export` from a laptop MCP client; expected: library skills only, no personal skill + slug. Revoke the token in the admin UI; the next call answers 401. +- [ ] Live, Claude AND Codex: in `cg-testing--bash` as Apps (approved, non-admin) send + “Create a personal skill token called laptop-test and show me how to use it.” Expected: the + reply shows a `cgs_…` value once, the endpoint URL and the Claude Code command. From a laptop, + `claude mcp add --transport http channelgate-skills --header "Authorization: Bearer "` + then `library_search_skills` lists the channel's section skill and Apps' personal skill, not a + skill of `cg-testing--admin` and not another person's personal skill; + `library_update_skill` on an organization skill answers “organization skills are changed by + admins”. Then “Revoke my skill token laptop-test”: the next laptop call answers 401. PASS when + both engines show the same, and the admin UI lists the token as kind user with Apps' id. +## Channel-level user API tokens (2026-11-03) + +- [x] Automated: `node scripts/run-tests.mjs test/api-tokens.test.js` (one file per invocation; + also `test/run-api.test.js`, `test/api-runs-channel-parity.test.js`) — store create/resolve/ + revoke/last-used and prefix shape; `/api/runs` accepts a channel token (validation 400 proves + authentication), answers 401 for a revoked token, 403 for a `channel` naming another + conversation, 403 when the creator is un-approved; the driver receives the creator as a trusted + author (`untrustedPrincipal:false`, `principalKind:"user"`) while the legacy key still yields + the API principal; job list/status/stop are scoped to the token's conversation; the admin + listing never carries a value; the MCP tools refuse a non-person principal and let only a + manager/admin see or revoke others' tokens. +- [ ] Live, Claude AND Codex: in `cg-testing--admin` (Admin mode) contact asks “Create an + API token for this channel named qa-admin”, Apps asks the same named qa-member. From a shell, + POST `/api/runs` with each token and the message “Which engine, model and role is this run using? + Then try set_channel_network on.” Expected: both runs are posted as threads in that channel; + contact's run reports admin rank and the network tool succeeds (audited with contact as author); + Apps' run reports member rank and the tool answers that only admins can run it. Then contact + revokes qa-admin (`revoke_api_token`) and the next POST with it answers 401; a POST with + qa-member and `"channel":"cg-testing--admin"` answers 403; a POST with the legacy + organization key still starts a run and the daemon log shows the deprecation warning. PASS when + every step holds for both engines. +## Run capability liveness (2026-11-03) + +- [x] Automated: `node scripts/run-tests.mjs test/capability-liveness.test.js` (then + `test/mcp-socket-server.test.js`, `test/egress-service.test.js`, `test/egress-secret-approval.test.js`, + `test/mcp-capability.test.js`, `test/gateway-mcp-authz.test.js`, `test/ssh-broker.test.js`, one file per + invocation) — the liveness-aware verifier admits a live channel, refuses an idle one with the fixed + sentence, audits once per token and tool per minute without the token, honours the 30 s + post-release grace and refuses after it; a background job, a memory review or an SSH session keeps + the channel live after the turn ended and another channel's work never vouches; the control socket + refuses the hello for an idle channel and accepts it while live; the remote-MCP jti registry still + authorizes; a wiring test pins every spawn path's registration (`run.js` lease → markLive → + bringRuntimeUp → finally release, background start and recovery, memory review, update smoke, + the broker's SSH source, every daemon-originated turn entering through `run.js`) and the socket's + verifier plus the chokepoint passing the tool name. +- [x] `npm run check:static`, `npm run secret-scan`. +- [ ] Live, Claude AND Codex (Worker fixture per engine, approved test actor): (1) "Write a small + Node script `probe.mjs` in this folder that connects to the gateway MCP bridge the way you do + (same env), sends tools/list, and prints the number of tools. Run it now." Expected: a tool count + is printed (live turn). (2) "Run `sleep 120; node probe.mjs` in the background." Expected: the + background job's log shows the same count (the job keeps the channel live after the turn ended). + (3) Wait until nothing is live in the channel (no turn, job, review, SSH; at least 2 minutes after + the memory review posted), then from an admin `/sudo` host thread replay the capability the + background job used (it is in the job's 0600 run env under the artifact dir; never paste it into + Slack) against the daemon socket with the bridge: expected one error frame whose reason contains + "This run's gateway capability is no longer active". (4) Warm-pool regression: two quick + consecutive turns in the same Claude thread both answer and both can call `list_secrets`. + (5) A schedule due in 2 minutes fires and its turn can call a gateway tool. (6) The memory + review after a turn posts its save notice (it runs with its own live registration). (7) With SSH + access granted, an SSH session open for 5 minutes can call `show_channel_ssh` at minute 4 while + nothing else is live. PASS only when every step holds for both engines; the audit feed shows + `capability_refused_idle` for step 3 only. + +## Catalog-first skill authoring instructions (2026-11-03) + +Validation: 42/42 existing workspace materialization and reconciliation checks passed. + +Documentation verification: compare the bundled gateway skills reference, `skill-authoring`, ChannelGate MCP/skills reference and `docs/SKILLS.md`: all require catalog creation, distinguish exports from active skills, allow read-only source use and require verified write access before source edits. Verify the existing workspace materialization regression suite still preserves the archive/replace behavior described here. No runtime enforcement or live engine acceptance is claimed by this instruction-only change. + +## Generated skill dependencies (2026-11-03) + +Validation: focused suite 25/25 passed; full `npm test` 4,144 passed, 33 skipped, zero failures; static checks and secret scan passed. + +Automated: `node --test test/run-grant-isolation.test.js` covers shared/personal dependencies on both generated protocols, preservation of unrelated omissions, and absent, manifest-less or symlinked sources. + +Live Claude and Codex acceptance (pending): in a dedicated writable test conversation pinned to each engine, grant a fixture skill `generated-protocol-probe` with `requires: [gateway-usage, channel-memory]`. Prompt: "Read the generated operating guide and memory protocol, then reply GUIDES-OK." Pass: both guides are readable and the streamed/final reply has no unavailable-skill warning. Repeat with the fixture granted only to the test author; the same pass rule applies and personal files remain isolated. Then add `requires: [gateway-usage, channel-memory, nonexistent-protocol-probe]`; pass only if the reply warns for the nonexistent dependency, omits both available guides from the warning and completes. Remove fixture grants afterward. Actual engine turns remain a deployment acceptance gate; unit tests are not a live pass. + +## Environment output visibility + +Automated: `test/egress-grants.test.js` checks auto-readable URLs, explicit Readable, hidden values, personal/organization/channel scopes and identical-byte collisions across active/inactive/no-target runtimes, character-split streaming, final and structured output. `test/egress-run.test.js` executes both engine parsers through `runMessage` with deterministic fake engine output. `test/runtime-integration-jobs.test.js` verifies readable URLs in persisted shell logs and recovered tails alongside hidden-value redaction. + +Live acceptance (pending, both Claude and Codex): private `cg-testing-claude-bash` and `cg-testing-codex-bash`. Add harmless `MAKE_BASE_URL=https://public.example.test` (auto-readable), `VISIBLE_API_KEY=visible-fixture-123` (Readable), `HIDDEN_API_KEY=hidden-credential-fixture-123` (Hidden). Prompt: “Read the fixture variables and return the base URL plus /8175/scenarios/123/logs/example as a Markdown link and plain text; include both fixture keys.” Pass: full URL and readable fixture survive streamed/final output; hidden bytes never appear. Repeat as a background shell and inspect persisted log and recovered tail; repeat with personal/organization scope and identical bytes in hidden/readable entries (hidden wins). Capture engine, message links/settings and job evidence; restore fixtures. QA registry case `ENV-READABLE-OUTPUT` covers both engines; automated success does not imply live acceptance. Validation: 4,135 passed, 37 skipped, zero failures in the full regression suite; static and secret-scan checks passed. The new Claude/Codex URL regressions both fail against the pre-fix source and pass with the fix. + +## Public VPS sizing guide (engine-independent documentation) + +- Contabo addition: verify GitHub/public guides agree on Cloud VPS 8 specs, shared CPU status, starting concurrency, source date and links. Validated 2026-11-03: 153-page build and 27,741 internal links; export preservation/idempotency; six existing domain-skill tests; DCO; secret scan. Preview `dpl_FDyYBt1h6jbBVcKeh5iVkkWXPwHF` and production `dpl_51edFqeHjYQBo5P4FS7cTrJytA89` passed 13 route/content/preservation checks each. Live browser verified the Contabo section and specs at desktop/mobile widths. Published source: `4a51680f762f256cdb84e8f781948be051fee21b`. + +- Build the documentation and run its export checks; verify the new route, canonical URL, sidebar, search index, installation/deployment links, and the production/pilot/busy sizing tables. +- Preview the combined website, then publish and verify `/docs/configuration/vps-sizing` plus installation/deployment entry points. Preserve existing website, blog, sitemap, and API configuration. +- Validation (2026-11-03): documentation build passed (153 pages, 27,738 internal links, Pagefind index); export preservation/idempotency checks passed. Independent source/navigation review found no blocking issues. Preview `dpl_2PsLP2yutu3VUEqKfqLjUpxY5GLT` and production `dpl_2cu28Ho4ZJXvRvS4CStvma1aiBr3` passed 13 route/content/preservation checks each. Live browser verified desktop (1280 px) and mobile (390 px) with no horizontal page overflow; Pagefind returns the VPS page first for “VPS sizing”. Published source: `70a8f5c4df378d6b1b24acbf736b8c2f7708f744`. DCO and secret scan passed. + + +Terra pricing regression: `test/usage-pricing.test.js` covers launch/snapshot pricing, strict >272K request uplift, aggregate fallback, upgrades from absent/older/October22 bases, backup-once and zero-delta reruns. Live production ledger rehearsal/integrity/balance verification remains pending host access; this is engine-independent accounting. + +## Stable sign-in candidate integration (2026-10-30) + +Combined with main's company-domain auto-approval change. Resolved Slack admission and Users +route conflicts while retaining code-channel routing, domain-based Approved-only grants, manual +opt-out and current admin-session validation after asynchronous user edits. The domain browser +fixture now uses an actual linked admin session and authentication router; no production gate +was relaxed to support the fixture. + +Local combined coverage run: 4,126 passed, 39 opt-in skipped, zero failed; line/branch/function +coverage 93.90% / 86.56% / 90.28%. Static, secret scan, security-coverage, DCO and high-threshold +production audit passed. Documentation build verified 152 pages and 27,245 internal links. +Existing physical-authenticator/live Slack delivery and other applicable beta live gates remain +unexecuted; private QA registry remains pending. These automated checks are not live acceptance. + +## Optional password and Slack code authentication (2026-10-28) + +- Automated scratch HTTP fixture: `test/admin-password-slack.test.js` and + `test/admin-password-slack-race.test.js`. Use fake private delivery, + disposable passwords and existing Slack Admin/non-admin users. Verify disabled-by-default, + password-only denial, correct browser-bound code login after passkey enrollment, replay/expiry/ + guess limits, private delivery failure, role and credential/settings revocation. Never send + fixture codes to deployed users. +- Browser fixture: `test/admin-password-slack-browser.test.js` exercises Settings → Admin access enablement, alternate sign-in and sensitive + reauthentication and sign-out on disable without a WebAuthn device. Chromium verification: + all 15 combined passkey/initial-setup/alternate-login assertions passed, zero skips; the + extended disable/sign-out browser fixture also passed. Private delivery is simulated. +- Live acceptance pending, engine-independent: this HTTP/browser authentication path never calls + either engine. On a disposable gateway with an existing Slack administrator and working bot DM + delivery, enable the checkbox using recent admin verification, sign out, enter the password + and own Slack member ID, then enter the privately received code. Pass only when the dashboard + opens as that exact existing user with no passkey prompt. Repeat a sensitive action with code + reauthentication. Confirm a wrong/replayed code and a non-admin are refused; disabling the + checkbox removes alternate login and invalidates its sessions. Check from the RustDesk browser + through a localhost SSH tunnel. Record screenshots/HTTP evidence without codes/passwords. +- Local regression: `npm run test:coverage -- --test-concurrency=4` passed with 4,120 + passed / 38 opt-in skipped / zero failed; coverage 93.91% lines, 86.55% branches, 90.27% + functions. An initial unconstrained run hit the container process limit and was terminated; + it is not counted as a passing run. Security-coverage, secret scan and production dependency + audit at the high severity threshold passed (three existing moderate advisories remain). +- CI fixture isolation: `test/web-security.test.js` now initializes scratch paths before + importing security helpers. Its isolated 15-test run under a fresh HOME created no runtime + files there; this fixes the existing real-home leak without relaxing the guard. +- Gate maintenance: linked-workspace authorization cases in `test/authorization.test.js` + restore the existing 95% branch floor (measured 96.81%); no production policy or floor changed. +- Private QA registry update pending: the requesting user's personal Airtable connection is not + connected in this run. Do not treat the missing registry or unexecuted live case as passed. + +## Linked working-folder setup acceptance + +Re-verification (2026-10-26): regression coverage now checks source `excludedUsers` and restricted-channel policy through linked DMs, explicit-unlink/source-departure section-skill retention, DM template preservation, established promoted-owner selection, and immediate follower SSH disconnection after source revocation (20-minute timer disabled as a fallback). `test/skills-workspace-sync.test.js` and `test/ssh-broker.test.js` cover these gaps. Updated full suite: 4,117 passed, 37 skipped, zero failures; Chromium linked-folder fixture passed. Actual Claude and Codex CLI smoke checks read a source-scoped skill from a generated linked workspace and returned its unique marker. Codex used the gateway's Worker/container mode; an initial nested read-only sandbox attempt failed with a bubblewrap capability error. These are CLI smoke checks, not a Slack end-to-end acceptance pass. + +Automated: `test/skills-workspace-sync.test.js`, `test/code-channels.test.js`, `test/workspace-conflict-reply.test.js`, `test/workspace-conflict-admin.test.js`, `test/workdir-boundary.test.js`. Verify automatic inheritance instead of skills/memory refusal, live source edits, owner-scoped skills, model independence, shared-write rejection, operator service secret filtering, established-owner selection, owner departure and explicit unlinking. `test/drivesync.test.js` covers sync selection; linked rows must never create a second bisync job. + +Live fixture (pending, run once with Claude and once with Codex): disposable private channels `cg-qa-linked-source` and `cg-qa-linked-follower`, same empty project directory, harmless test skill and memory sentinel. Set source Worker/network on; give follower different initial skills/memory. Select source folder from follower in Admin UI. Confirm source label/button, inherited controls, no conflict error, equal durable skill tree and source-scoped skill. Prompt follower: “Report your configured engine/model and the available shared test skill; add a test memory fact.” Change source skill/memory, retry, then change follower harness/model and confirm only runtime changes. Reject follower shared-setting edits with source guidance. Open an approved code channel from source and confirm it starts. Unlink follower and verify independent settings resume. Record message links, settings screenshots and engine evidence; no live PASS is claimed by automated checks. Restore/delete disposable fixtures. Cross-platform fixture: share a source with a Teams/Google Chat follower and verify actual-surface preamble while owner guide bytes remain stable. Drive fixture: valid disposable Drive folder, one scheduled job and one state directory, follower manual sync resolves to owner. + + +Running label acceptance (Claude and Codex): in a Slack agent thread with chat:write.customize, select an explicit effort using /model, then ask “Read README.md and explain the startup flow.” Confirm Stop shows the selected model (effort), transitions through live tool/writing phases, and becomes active without an activity suffix after completion or Stop. Repeat with each engine and a different effort; verify fallback replaces model/effort. Pass only with observed Slack evidence. Live execution pending; automated coverage: test/slack-progress.test.js native Stop effort/phase test and test/slack-stop-progress.test.js late-event cancellation fixtures. + +## User-linked dashboard access (2026-10-25) + +- Recovery proof revocation regression (engine-independent): run + `node --test test/admin-recovery.test.js` against scratch state and the actual recovery CLI. + Issue a private identity proof from an unlinked container for an existing canonical admin, + then recover that user with `--user`. Test both an empty invited target and a target whose + first key was enrolled after proof issuance. The old proof and invitation must be invalid, + the new registration-only link must still name the recovered container, its old keys must + be gone, and another user's unrelated pending proof must still verify. No deployed recovery + or native private-message delivery is performed by this regression. +- Secret reveal identity regression (engine-independent): run `node --test test/secret-reveal.test.js` + against its scratch store and actual admin router. A linked administrator's fresh session reveals + a synthetic allowed setting, requests a forbidden setting, then repeats after six minutes. + Require statuses 200/400/403 and exactly one respective `secret_revealed`, + `secret_reveal_rejected`, or `secret_reveal_denied` event with the account actor and canonical + `userId`. Forged request-body user IDs, secrets, passwords and session cookies never enter audit + metadata. Unlinked/bootstrap audit behavior remains covered separately; no live secrets are used. +- Automated acceptance uses scratch databases and Chromium virtual resident authenticators: + legacy-key identity linking preserves credentials; initial setup requires proof when admins + exist; existing Users drawer invites another admin; non-admin/API identities cannot enroll; + demotion/re-promotion invalidates sessions, invites and pending signatures. Private codes + enforce same-session binding, expiry, five attempts and replay prevention. Old independent + account creation is retired. Run `test/admin-user-identity.test.js` and the existing admin + storage/HTTP/recovery/browser suites. +- [x] Final combined storage/HTTP/CLI/migration/Chromium acceptance: 43/43 pass, zero skips. + Includes actual signed virtual-authenticator flows for both initial and legacy migration; + fake private delivery tests never contact deployed users. Migration 37 leaves existing + public-key bytes, counters and opaque user handles intact and revokes historical setup links. +- [x] Independent review plus an HTTP race regression: an actor demoted during asynchronous + user validation cannot finish a pending self-promotion. Static checks (946 JavaScript files), + secret scan and security-coverage gates pass. +- [x] Full `npm test`: 4,099 passed, 33 opt-in skips, zero failures. The question-continuation + fixture now disables its unrelated background memory reviewer; its 12 assertions also pass + independently without starting that external process. +- Private QA: all seven existing passkey case definitions updated for the new identity model; + human native-chat proof and deployed physical-key acceptance remain pending. +- [ ] Live human acceptance, engine-independent: sign in with an existing physical passkey, + verify its owner through the native private chat code, then invite a second existing admin + from Users. Confirm independent enrollment, username-free sign-in and role revocation. No + virtual authenticator may replace deployed keys; automated tests do not satisfy this gate. + +## Passkey-only account discovery (2026-10-25) + +- [x] Combined Chromium/storage/HTTP/recovery acceptance: set `CG_BROWSER_MODULE` to installed + Playwright and run `node --test test/admin-passkeys-browser.test.js test/admin-login-assets.test.js + test/admin-auth-http.test.js test/admin-accounts.test.js test/admin-recovery.test.js` — 25/25 + pass with no skips. Both independent virtual authenticators use the login button without a + username field, and `/api/auth/me` identifies their correct accounts. Public options contain + no credential directory. Missing or another account's user handle, invalid signatures, + replay, disabled/revoked accounts and stale pending challenges cannot mint a session. + Password login remains rejected after first enrollment and recovery. +- [x] Independent security review: generic login challenges carry only server-side account-version + snapshots; the submitted credential resolves its owner and must carry that owner's user handle; + signature/UV/origin/browser/expiry checks and post-verification revocation guards remain intact. +- [x] Focused account/authentication, web hardening, secrets, settings and migration regressions: + 84/84 pass with no skips. Static checks and the secret scan pass. +- [ ] Live, engine-independent: after deployment, in a fresh browser at the configured HTTPS URL, + click **Sign in with a passkey**, select a previously enrolled account, and complete device + verification. PASS when the dashboard opens without typing a username and password setup is + absent. Repeat with a separately enrolled administrator and verify the correct account label. + +## Individual admin accounts and passkeys (2026-10-25) + +- [x] `node --test test/admin-accounts.test.js`: 7 storage regressions cover username uniqueness, + opaque user handles, permanent password retirement, digest-only expiring single-use invitations, + revocation races, credential ownership, counter compare-and-swap, final key and final admin. +- [x] With `CG_BROWSER_MODULE` pointing to installed Playwright, run + `node --test test/admin-passkeys-browser.test.js`. Chromium uses a disposable CTAP2 virtual + authenticator in a scratch runtime. All 11 scenarios plus their parent pass: actual initial + password form → passkey creation; old password/session retirement; passkey-only sign-in; + browser binding, invalid signatures and challenge replay; UV/ceremony/origin/CSRF rejection; + expired challenges and stale-session reauthentication; second admin invitation/URL-fragment + cleanup/registration-only authority/account mismatch; disablement revoking sessions and + in-flight assertions; spare-key replacement; and host recovery invalidating old authority + before a new key can sign in; and server-side 30-minute idle/12-hour absolute expiry despite + repeated keepalives. No physical key is registered in the deployed gateway by tests. +- [x] Independent security review covers exact origin/RP, required UV, single-use/browser/session + binding, durable password retirement, atomic enrollment/counter updates, revocation races and + host-only recovery. Secret reveal rechecks mode/session after asynchronous verification/read. +- [x] `npm test`: 4,076 passed, 32 existing opt-in skips, zero failures. The new always-on HTTP + and recovery CLI tests pass separately (4/4), including `.env` selection of a scratch runtime. + Final combined storage/HTTP/CLI/Chromium acceptance passes 25/25 with zero skips. +- [x] `npm run check:static`, `npm run secret-scan`, `npm run test:security-coverage`, and + `npm audit --omit=dev --audit-level=high` pass; npm reports zero vulnerabilities. +- [ ] Live, engine-independent, human enrollment: at the configured HTTPS Public URL in a fresh + browser, open first-time setup, use the existing password for `admin`, then register a real + device/security-key passkey with PIN/biometric verification. PASS when password login disappears, + logout and passkey-only sign-in work, a spare authenticator can be added, and an invited + second admin enrolls independently. Existing stored passkeys must never be replaced by virtual + authenticators. Browser authentication invokes neither Claude nor Codex, so one web acceptance + case covers both engines. Automated virtual-authenticator results are not human acceptance. + +## Signed-out admin login assets (2026-10-25) + +- Login cache regression: HTML uses `no-store`; both login/passkey scripts carry the same resolved content stamp. Run the asset HTTP/browser and versioning suites to prevent a pre-deploy username-reading script pairing with the new form. + +- [x] HTTP regression: `node --test test/admin-login-assets.test.js` serves every script, icon and + font referenced by the real login HTML without cookies, with correct MIME types and WOFF2 + signatures, including HEAD and versioned requests. Dashboard assets still redirect to login + and `/api/settings` still answers 401. The regression fails against the previous auth gate. +- [x] Browser regression: set `CG_BROWSER_MODULE` to an installed Playwright module and run + `node --test test/admin-login-assets.test.js`. A fresh browser with no cookies opens `/`, sees + sign-in, submits a wrong password and sees its error, then submits the disposable fixture + password and reaches the authenticated dashboard. Log out and repeat sign-in. PASS when both + sign-ins work, the session is HttpOnly/SameSite=Strict, and there are no page script errors. + Executed with headless Chromium: all 24 tests passed across this file, + `test/admin-auth-bind.test.js` and `test/web-security.test.js`, with zero skips. +- [ ] Live, engine-independent: open the deployed public gateway in a fresh browser context; + verify the login script and three font requests return 200 with actual assets, enter the admin + password through the form, reach the dashboard, log out, and sign in again. Verify no + `Unexpected token '<'` or font decode errors. Browser/HTTP authentication does not invoke an + engine, so Claude and Codex share this acceptance case. + +## Work-folder boundary and protected export names (2026-10-25) + +- [x] Automated (`node scripts/run-tests.mjs `, one file per invocation): + `test/workdir-boundary.test.js`, `test/public-link-protected-names.test.js`, + `test/public-file-links.test.js`, `test/slack-share-file.test.js`, `test/channel-policy-audit.test.js`, + `test/author-runtime-isolation.test.js`, `test/channel-workdir-ui.test.js`, + `test/mcp-control-plane-approval.test.js`, `test/workspace-read-tools.test.js`, + `test/teams-reply-menu.test.js`, `test/googlechat-controls.test.js` — the predicate for every listed root in both directions, + the channel's own folders allowed, other conversations' default folders and their parents + refused while their custom folders, parents and subfolders are allowed, the store-backed list + (default folders only); the strict and lenient export predicates; `set_channel_workdir` + and the admin PUT answer `Refused: … is ` / `workDir refused: …` and write nothing; the + runtime resolver throws the remedy for a stored forbidden value while an ordinary custom folder + still mounts; the export predicate, `openConfinedFile` by requested and physical path, both link + purposes, staging, `slack_share_file`, and a pre-existing link to `.env` answering 404. +- [x] `npm run check:static`, `npm run secret-scan`. +- [ ] Live, engine-independent (admin UI + MCP): in a private test channel, as an admin, set the + Working folder in the admin UI to the gateway account's home directory, then to `~/.channelgate`, + then to another registered conversation's folder. Expected: each save answers a 400 whose + message starts `workDir refused:` and names the reason; the stored value is unchanged. Then in + the channel ask the agent: “Set this channel's working folder to ~/.channelgate.” Expected: the + tool answers `Refused: … is inside the gateway runtime root …` and nothing changes. Finally set + a real project directory: accepted. PASS when all four hold. +- [ ] Live, Claude AND Codex: in a Worker channel with public file links enabled, create `.env` + (one dummy line) and `report.md` in the folder, then ask (one fresh thread per engine): + “Create an upload public link for .env, then one for report.md.” Expected: the first answers + `Public link refused: … protected file …` with no URL; the second returns a URL that downloads + `report.md`. Then: “Share .env here as a file.” Expected: `Sharing refused: … protected file`. + Then: “Share MEMORY.md here as a file.” Expected: the file is posted in the thread (the lenient + in-channel door). PASS when both engines show both refusals and the two successes. +## Access policy and runtime controls consistency (2026-10-25) + +- [x] Automated: `node scripts/run-tests.mjs test/access-settings.test.js + test/access-policy-eviction-web.test.js test/settings-runtime-policy.test.js + test/authorization.test.js test/channel-settings-modal.test.js test/slack-settings-redesign.test.js + test/channel-engines-web.test.js` — narrowing to "admins"/"none" drops seeded approved members + and keeps explicit guests (pure helper, Slack save with audit count and people-row ticks, web + PUT), widening resurrects nobody, admins-only → locked moves nothing more; `canEditRuntime` + follows the policy (member false under the default, true under "users", DM owner always), the + page renders the 🔒 line without pickers, the select and thread-reset handlers call the policy + check before any write. +- [ ] Live, Slack, engine-independent: in a test channel whose Access page shows an approved + non-admin member ticked under "Approved members", switch "Who can use it here" to "Admins only" + as an admin. Expected: the save notice says one previously admitted member was removed, the + member's row is now unticked, and a message by that member is refused with the + "restricted to admins" reason. Switch back to "Approved members": the member's next message is + admitted again (re-seeded). PASS when all three hold. +- [ ] Live, Slack, Claude AND Codex: with Settings → model-change access at "admins", open ⚙️ + Settings as an approved non-admin member. Expected: the Agent page shows the channel default as + text with the 🔒 policy line and no engine/model/effort pickers, and `/model` in the channel is + refused. As an admin, switch the channel default to the other engine from the same page; the + member's next turn in a fresh thread reports the new engine. Repeat with the policy at "users": + the member's pickers appear and a member's switch takes effect. PASS when both engines show the + expected spawn evidence. +## Login backoff behind a reverse proxy (2026-10-25) + +- [x] Automated: `node scripts/run-tests.mjs test/web-security.test.js test/settings-save-version.test.js + test/admin-auth-bind.test.js test/approval-links.test.js` — `clientKey` ignores both forwarded + headers with no proxy declared (loopback and non-loopback sockets alike); `cloudflare` reads + `CF-Connecting-IP` only; `xff` reads the LAST `X-Forwarded-For` hop; the stored setting wins over + `CG_TRUST_PROXY`; legacy `1`/`true`/`yes` mean `xff`; an unknown value trusts nothing; the global + cap holds fresh keys at the maximum delay after N failures and drains with the window, is + switchable off, and defaults to 50 per 10 minutes; the admin API persists `""`/`cloudflare`/`xff`, + maps `1` to `xff`, refuses unknown values with 400 and writes nothing; the Settings page paints + and saves the select. +- [x] `npm run check:static`, `npm run secret-scan`. +- [ ] Live, engine-independent (browser + curl): on the deployment (Cloudflare tunnel in front) set + Settings → Connection → Behind a reverse proxy to **Cloudflare tunnel** and save. From a browser + through the public hostname enter three wrong admin passwords; the fourth attempt is delayed + (the page reports the wait) while a colleague on a different network can still sign in at once. + Then, against the LAN/loopback bind only (Cloudflare overwrites the header on the public path), + `for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:4747/api/login + -H 'content-type: application/json' -H "CF-Connecting-IP: 10.0.0.$i" -d '{"password":"wrong"}'; done` + with the setting at **None**: every request shares the loopback bucket, so the 4th onwards is + 429/delayed regardless of the spoofed header. PASS when both hold and the setting round-trips + after a daemon restart. +## Web and Slack surface hardening, batch 1 (2026-10-25) + +- [x] Automated: `node scripts/run-tests.mjs test/web-hardening.test.js test/access-settings.test.js + test/admin-auth-bind.test.js test/admin-navigation.test.js test/run-control-auth.test.js + test/file-explorer-auth.test.js test/asset-versioning.test.js` — `/API/...` refused by the + lockdown / 401 branch and never routed; CSP + nosniff + frame denial on the shell and login page, + the import-map hash matching the served map, no inline script or handler in either page; a + malformed JSON body answers a bare 400 and the final 4-arity error handler is registered; the + internal IPC secret compared with `timingSafeEqualStr`; a manager's Settings save that selects + Admin is refused and writes nothing while an admin's succeeds, a manager keeps Admin when + changing flags and may step down, the picker lists "Admin" only to an admin or when current; + the `cg_bgjob_status` handler calls `authorizedControlEntry` and compares the job's slug; share / + send-to-me pass `rootPath`, preview and edit open through the `/proc/self/fd` re-check; the + License card escapes; the unit sets `NODE_ENV=production`. +- [x] `npm run check:static`, `npm run secret-scan`, `npm audit --omit=dev` (0 vulnerabilities). +- [ ] Live, engine-independent (browser): sign in to the admin UI through the deployment's public + hostname. Expected: the page renders and navigates normally (Overview, Conversations, Settings, + Skills, System health); the browser console shows no CSP violation; `curl -i https:///` + shows `Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-…'` and + `X-Frame-Options: DENY`; `curl -i https:///API/settings` answers 401 (password set) with + no settings payload; `curl -i -X POST https:///api/login -H 'content-type: application/json' + -d '{bad'` answers 400 with `{"error":…}` and no stack. PASS when all four hold. +- [ ] Live, Slack, engine-independent: in a test channel with `manageAccess: members` open Settings + as an approved NON-admin member. Expected: the Mode picker offers Read-only and Worker only; + switching Auto on succeeds and the channel stays in its mode. As an admin, pick Admin: the channel + enters Admin mode. Back as the member: the picker now shows Admin (current), ticking Network + succeeds and the channel stays in Admin mode, picking Worker steps it down. PASS when every step + matches and the audit events name the clicker. +- [ ] Live, Slack, Claude AND Codex: in a Worker channel start `run_in_background` with + `sleep 90; echo done` (one run per engine). As an approved member click "Check status": an + ephemeral status appears. As a workspace member who is NOT approved and not a guest of the + channel, click the same button: nothing is posted. PASS when both hold for both engines. + +## Slack code channels (2026-10-25) + +- [x] Automated: `node --test test/code-channels.test.js test/slack-agent-sessions.test.js + test/mcp-control-plane-approval.test.js test/folders-settings.test.js test/platforms.test.js + test/channelgate-rename.test.js test/slack-progress.test.js` — one channel-wide session key that + is never a Slack ts, top-level vs in-thread reply target, mode decision from the folder with + requester override, inherited meta (origin policy, secrets, skills, folder; own identity; no DM + fields), re-projection on sync without needless writes, worktree creation from the upstream or + exact HEAD with branch-name suffixing and `info/exclude`, diff of committed and uncommitted work + excluding the managed instruction block, Slack create/status/context-bar payloads, eligibility + error mapping, view validation (key, PR URL, size, type), diff published once per distinct + content, archive with wrap-up ts and `archivedAt`, channel-level status/rename without + `thread_ts` or the legacy bridge, thread-less Stop sweeping a code channel and ignored elsewhere, + `open_code_channel` as an owner-tier card whose saved call opens the channel and kicks off the + first turn (refused from a synthetic session key or inside a code channel), `update_code_channel` + refusing outside a code channel and publishing title/context/tabs, `archive_code_channel` as an + open-tier card, the Slack-only capability, guide file and routing row, manifest feature and + scope, and the tool inventory (the GATED/OPEN tripwire and `GATEWAY_TOOL_NAMES`). +- [ ] Live Slack acceptance (engine-independent for creation; Claude and Codex for the turns): + paste the updated `slack-app-manifest.json` into the Slack app (adds `features.code_channels` + and `code_channels:manage`), reinstall, restart the daemon. In a private channel fixture whose + work folder is a git repository, as an approved member mention the agent: "Open a code channel + to add a CHANGELOG entry for X." Expected: a card in the thread naming the task and "a task + branch in a git worktree…", approvable by the requester; no channel exists before the click. + Approve. Expected: a new channel named from the request appears in the Code channels sidebar + section, the requester is a member, the context bar shows the folder and `code/`, the + session title is set, and the agent posts its first top-level message within the turn budget + (the turn's preamble carries `[Slack code channel]`; verify from the run log). If Slack answers + `feature_disabled` or `user_not_enabled`, the card outcome must name it and the remedy — record + that as the eligibility result for the workspace and stop. Send "Change the heading too" without + a mention: the agent answers top-level. Press Slack's Stop while a turn runs: the run stops + (`run_stopped` event with threadKey `code-channel`). After a turn that edits a tracked file, + open the Code tab: the diff shows that change and not the gateway's managed block. Ask for an + HTML preview: the tab appears via `update_code_channel`. Ask to archive: a card appears; approve; + the wrap-up is shared into the origin thread and the channel is archived; `.worktrees/` + still exists on the host. Repeat the opening in a non-git fixture folder: the card says "this + channel's own files" and no worktree is created. Pending live execution. + +## Harness default effort (2026-10-23) + +- [x] Automated: `node --test test/default-model.test.js test/qwen-provider-settings-route.test.js + test/engine-registry.test.js test/engine-adapter-contract.test.js + test/runtime-identity-preamble.test.js` — settings round-trip, legacy/invalid/blank handling, + atomic API validation for every harness, fresh/resumed CLI flags, request > thread > channel > + gateway > CLI precedence, plus destination defaults through failover and model retry. +- [x] Browser: `CG_BROWSER_MODULE=/absolute/path/to/playwright/index.mjs node --test + test/qwen-provider-settings-browser.test.js` — disposable admin API and real Chromium; + save/reload/clear distinct Claude, Codex and Qwen EU efforts without changing production settings. +- [ ] Live Claude and Codex acceptance: for each engine, use a private read fixture with that + engine selected and model/effort unset on the channel and thread. Record existing gateway + defaults; in Admin UI → Agent defaults set the engine's model and effort to a supported pair + (Claude sonnet/high; Codex gpt-6-sol/medium). Start a fresh thread as an approved test actor: + “Which configured engine, model and effort are you running? One line.” Check captured runtime + facts and actual engine flags, not just the answer. Change gateway effort to low, send “And + now?” in the same thread, and verify the resumed turn receives low. Set channel effort medium, + then thread effort high, then request effort low through the Run API; each more specific choice + must win. Clear each override and the gateway effort; verify the effort flag is omitted. Restore + the recorded defaults. PASS only when actual spawn evidence agrees at every step for both + engines. Repeat replay-safe failover with different source/destination defaults and verify the + destination uses its own effort. Pending live execution; automated stubs are not live engine PASS. + +## Admin access to Slack buttons and forms (2026-10-23) + +- [x] Automated: `node --test test/admin-menu-controls.test.js test/file-button-actions.test.js + test/menu.test.js test/question-admin-access.test.js test/question-interactions.test.js + test/question-continuation.test.js test/scheduled-questions.test.js + test/run-choice-admin-access.test.js test/busy-thread-choice.test.js + test/engine-switch-choice.test.js`. + Real scratch stores and captured Slack calls verify admin overrides for all three menu controls, + actual viewer ownership/personal variable isolation, legacy Resume, question choices, custom text, + multi-select, paged forms, Back, cancel and submit. Foreign ordinary members, demoted admins, + revoked requester access, missing membership, wrong cards/channels and stale revisions fail closed. + Admin answers preserve the original continuation author and audit the responder without answers. + Busy-thread/engine-switch fixtures verify admin access, original incoming-event identity and + responder audit, plus rejection for ordinary foreign members, demotion, removed membership, + dormant access, a different channel and duplicate decisions. +- [x] Final development validation: `npm run test:coverage` — 4,025 passed, 34 skipped, + zero failures; coverage floors passed (lines 94.30%, branches 86.65%, functions 90.28%). + `npm run check:static`, `npm run secret-scan`, `npm run test:security-coverage` and DCO passed. + Corresponding private QA definitions are updated; live acceptance remains unexecuted. +- [ ] Live menu acceptance (engine-independent; no engine turn for these controls): in a disposable + Slack channel, admit an approved member, a second ordinary member and a current organization + admin. Give the first member and admin distinct disposable personal variables. As first member, + post `@agent /menu`. As admin, open Files, Variables and Settings from that card and from an + existing member-authored reply. Require every modal to open; Variables/Settings → You must show + only the admin's personal entries. Open and save a disposable channel-variable form and remove + the variable afterwards. Require audit attribution to the admin and no raw secret in a view. + As second ordinary member, require foreign owned buttons denied. Demote the admin or remove + channel membership and retry: deny the foreign button; also deny stale personal modal actions + after membership is removed. Record Slack view payloads, actor ids and audit event ids. +- [ ] Live question acceptance (repeat with Claude AND Codex): pin each engine in its disposable + Worker fixture. As approved member ask: “Use ask_questions to collect audience and project name + before replying; allow custom text.” As a current member admin, select an option, open custom + text, save and submit. Repeat with six questions to exercise Back/Next; cancel a separate request. + Require one continuation in the original thread/session as the original requester, their personal + scope and project runtime lane, plus `question_admin_action` naming the actual admin with no + answer text. A second ordinary member must be refused. Repeat after admin demotion, requester + access revocation and either participant leaving: no continuation. Stale/replayed submission + must never duplicate a run. Capture question id, Slack payloads, run author/session/runtime, + credential names only and audit events. No live result is claimed by the automated fixtures. + +- [ ] Live shared run-choice acceptance (Claude AND Codex fixtures): as a member start a harmless + long turn, then send a second message to obtain the busy-thread card. As member admin click Queue, + then repeat separate fixtures for Steer and Cancel Request. Require one original-author message + queued/accepted (or durably discarded for Cancel), preserved personal scope/runtime lane and + `run_choice_admin_action` naming the admin with no original message text. The existing + cross-author automatic steering guard must remain intact. With ask-before-switch enabled and a + disposable replay-safe fake-provider failure, have the admin click Switch and Try again in + separate fixtures; require the original author to continue once on the selected harness. + Ordinary foreign members, demoted/departed admins, dormant channels and wrong-channel cards + must fail without consuming the pending choice. Repeat a successful click: no duplicate run. + Capture Slack card, pending-choice id, run identity and audit events; no live pass claimed here. + + +## Explicit readable secrets in strict mode (2026-10-22) + +- [x] Regression: `test/egress-grants.test.js` exercises organization, personal and conversation + entries in proxy + strict mode. Explicit readable API keys (including catalog, declared and + approved-host metadata) are raw and `unprotected`; auto passwords and hidden password-like + entries stay absent and `withheld`; hidden API keys remain placeholders. Every real value stays + in the inventory; hidden values remain in redaction inputs and Readable output follows the + existing visibility policy. Hidden/auto reversals apply on the next resolve, and an untrusted + principal receives no personal secrets. A lower-scope readable entry cannot authorize a + higher-scope auto entry. +- [x] Regression: `test/secrets-tool.test.js` distinguishes automatic WITHHELD findings from + **Explicit readable** raw injection, without full values; summaries honor scope precedence. + Existing tests verify the scoped caller authority and durable readable approval-card tiers. +- [ ] UNEXECUTED — live Claude AND Codex, each in a separate Worker + Auto QA conversation with + proxy egress, Network on, strict mode on, an approved member allowed to manage it, and synthetic + conversation variables `QA_CLI_API_KEY` (hidden) and `QA_DB_PASSWORD` (auto). First prompt: + "List the secrets and check only whether QA_DB_PASSWORD is present; never print either value." + Pass: password absent and reported WITHHELD; API key is a hidden placeholder. Next prompt: + "Make QA_CLI_API_KEY readable in this conversation for a CLI that needs its raw value." + Require the real durable card; before approval the stored mode stays hidden; deny once and + confirm no change. Repeat and approve with an eligible human. In a fresh turn: + "Check only whether QA_CLI_API_KEY is present and starts with cgph_; show no values, then list + secrets." Pass: present, not a placeholder, **Explicit readable** listing; QA_DB_PASSWORD still + withheld and strict remains on. Restore QA_CLI_API_KEY hidden, then test QA_DB_PASSWORD explicit + readable → auto in separate fresh turns: raw only while explicitly readable. Verify another + conversation receives neither variable. Restore/remove the fixture variables and retain only + value-free evidence. Live execution and private QA registry updates remain pending access. ## Email accounts (2026-10-22) @@ -104,10 +817,10 @@ inbox). Record the address and the servers; never the password. `invalid_arguments — [ERROR] unsupported element: multiselect [json-pointer:view/blocks/7]` before the fix; it must open after it. - [x] Automated `test/license-registration.test.js`: signed Registered and no-key fixtures admit - 19→20 users/channels and refuse 20→21 atomically, including a two-process race for the last + 9→10 users/channels and refuse 10→11 atomically, including a two-process race for the last slot. Count every stored conversation (DMs/API included), every stored user (approved or not) except the fixed synthetic API principal. Enterprise defaults remain unlimited; old free - payloads cannot raise the 20 ceilings. Downgrade preserves 25 existing records while blocking + payloads cannot raise the 10 ceilings. Downgrade preserves 25 existing records while blocking additions. Slack, Teams, Google Chat, admin user API and run API return readable refusals. - [x] Automated `test/channel-engines.test.js`: malformed/empty approvals fail closed, changing or clearing approvals requires Enterprise, whole-record saves cannot erase the policy, @@ -128,9 +841,9 @@ inbox). Record the address and the servers; never the password. Dependency audit passes the high-severity threshold (two pre-existing moderate advisories). - [ ] Live inventory fixture, **Claude and Codex** independently: use an isolated daemon with a legitimately signed Registered key, 19 stored conversations and 19 stored human users. Register - the 20th then attempt the 21st through admin API and chat on every connected surface. Include a + the 10th then attempt the 11th through admin API and chat on every connected surface. Include a DM, API conversation and unapproved account; repeat concurrent last-slot attempts. **Pass:** - exact counts remain 20, refusals appear without engine/session/usage writes, existing users and + exact counts remain 10, refusals appear without engine/session/usage writes, existing users and channels still work. Repeat Enterprise→Registered with 25 records: retain and edit all existing records, refuse additions. Never change a production deployment's license for this drill. - [ ] Live harness controls, **Claude and Codex**, paired private Read/Admin fixtures: as an @@ -149,8 +862,59 @@ inbox). Record the address and the servers; never the password. Private QA definitions: LICENSE-INVENTORY-20-01, HARNESS-APPROVALS-01 and HARNESS-APPROVALS-AUTOMATION-01. Definitions were created and reread; live runs remain pending. +## Channel-to-channel agent mail (2026-11-06) + +- [x] Regression: `node --test test/agent-mail.test.js test/agent-mail-tools.test.js + test/agent-directory-tools.test.js test/mcp-control-plane-approval.test.js + test/migrations.test.js`. Covers target resolution and ambiguity; the complete exchange (gate at + send, delivery and reply; attributed and defanged kickoff; the `agent:` principal with + `untrustedPrincipal`, the `agent-message` toolset and the `agent_message` origin; the reply + posted in the target thread and re-injected into the origin thread as a continuation by the + requester); every refusal (requester not a member, not authorized, admin without membership, + stranger, DM source, target not accepting, hidden, unregistered, self, ambiguous name, oversized, + empty, non-thread key, disconnected connector); in-flight, per-pair and per-source caps with the + one-hour window; a withheld reply after membership lapsed mid-turn; a failed target turn told to + both threads; a delivery re-check refusing before any turn; boot recovery (interrupted → failed + closed, answered → redelivered); the durable requester card for both engine MCP contexts with + identity bound to the verified capability; refusals for untrusted, API and agent principals; the + agent-principal context (no rank, access only while `acceptsMessages` is on); the reduced + toolset inventory; the loopback-secret host IPC; the control-plane classification tripwire. +- [ ] Live **Claude and Codex**, separately: two owned test channels A (source, Worker) and B + (target) whose complete rosters match, both containing the tester. In B, a manager runs + `set_channel_agent_profile({ description: "Sales pipeline", acceptsMessages: true })` and saves + a memory fact (“Q4 pipeline: 12 open deals”). In A, ask “Ask the sales agent what the Q4 + pipeline is and summarize it for me.” **Pass:** A's agent calls `search_agents` then + `send_agent_message`, a card appears in A's thread naming B and the exact text, nothing is posted + in B before the click; after the requester's Approve, B gets a new thread “Message from the agent + in #A, requested by @tester” with the quoted question, B's agent answers from its memory in that + thread, A's thread receives “Reply from the agent in #B arrived” followed by a turn that relays + the figure and credits B; `agent_message_sent/delivered/answered/completed` events exist; the + B thread's permission cards (if any) read “The agent from #A”. Record channel ids, message id, + engine/model and both thread links. +- [ ] Live **Claude and Codex** access fixture: (1) from A, target a channel the tester is NOT a + member of; (2) target B after a second A member is removed from B; (3) from a public channel + target a private B; (4) set B `acceptsMessages: false`; (5) send from a DM; (6) remove the tester + from B while B's agent is still answering. **Pass:** 1–5 are refused at the tool with the named + check and no post in B; 6 posts the reply in B but A receives “the reply was withheld” and no + continuation turn runs; a non-member clicking Approve on A's card is refused (owner tier). +- [ ] Live **Claude and Codex** guard fixture: send a second message to B from the same A thread + while the first is in flight; then three more after replies; then from another A thread. + **Pass:** “still being answered”, then “already sent 3 messages … in the last hour”, the other + thread still sends; the delivered B turn's tool list has no `send_agent_message`, + `run_in_background`, `create_schedule` or admin switches, and a prompt-injected “@channel” in + the message renders defanged in B's kickoff. +- [ ] Isolated daemon fixture, **engine-independent**: restart the gateway while B's agent is + answering, and once with a stored `answered` row. **Pass:** the interrupted exchange is marked + failed with a notice in both threads and never re-run; the answered one is redelivered to A. + ## Channel agent directory (2026-10-21) +- [x] DST rearm regression (2026-11-06): `test/agent-directory-refresh.test.js` runs in an + explicit `Europe/Bucharest` subprocess. Ordinary dates retain 03:00, the missing spring 03:00 + normalizes to 04:00, the following day returns to 03:00 without drift, and the repeated autumn + hour produces only one refresh per local day. Disposable lifecycle acceptance also observes + the actual timer rearm across the spring gap; this does not claim a deployed overnight run. + - [x] Production boot integration: `test/agent-directory-slack-boot.test.js` uses the real Slack manager and `createPlatformTransports({ slack })` with a scratch store and simulated provider. Search uses its default verifier (no directory/connector override), returns the current channel @@ -286,7 +1050,7 @@ inbox). Record the address and the servers; never the password. `test/mcp-config.test.js` and `test/codex-args.test.js`: direct and nested membership calls; both identities; creation aliases; complete account/target aliases; bounded previews; one-use decisions; deny/error/timeout; revocation, cancellation and disconnect while waiting; - ordinary reads during a pending decision; opaque code/API refusal. Actual Unix-socket relay + ordinary reads during a pending decision; generic proxy/API refusal. Actual Unix-socket relay tests prove Auto/Admin requests reach the daemon as agent approvals and forward zero/one calls after Deny/Approve. Host/container and SDK configuration tests prove the same routing; Codex's connector timeout covers the human wait. @@ -300,12 +1064,32 @@ inbox). Record the address and the servers; never the password. before clicking its card. Repeat token/SDK connections and the explicit host fixture. **Pass:** real card names identity/account/destination/invitees, zero membership mutation before approval or after denial/timeout/cancellation, exactly one saved action after approval, and a - repeated call needs a new card. Requested sender stays unchanged. Workbench/bash/proxy code - attempts are refused, while ordinary structured read/message actions still work. Capture exact + repeated call needs a new card. Requested sender stays unchanged. Generic proxy/API + attempts are refused; Workbench processing and structured read/message actions still work. Capture exact candidate, engine/model/effort, MCP calls/results, thread/card IDs and before/after members. Hermetic tests do not count as live passes. Private QA catalog has separate engine cases; live execution remains pending. +### Workbench restoration (2026-10-23) + +- [x] Automated: `test/composio-approval.test.js` covers both identities, repeated Workbench calls + with an unchanged existing workflow ID and remote file reference, nested Workbench forwarding, + a later membership action in the same batch still requiring approval, revoked authority, + and continued rejection of other generic executors and Workbench lookalike names. Focused + approval/socket/configuration regression passes 53/53; static and secret-scan checks pass. +- [ ] Live **[Claude] and [Codex]**, separately: private QA Auto fixture with an approved test actor + and a configured fixture Composio connection. Prompt: “Read the harmless fixture records, + save the result remotely, and use Composio Workbench to calculate their total. Give me the + workflow session ID and checkpoint reference.” Resume the same thread: “Use that same session + and checkpoint to recalculate the total without fetching again.” Then request a structured + membership action to a disposable QA destination and deny its card. **Pass:** Workbench + returns the correct total twice from the same session/file with no blanket gateway refusal; + the structured membership action still shows its one-use card and is never forwarded after + denial. Capture candidate, engine/model/effort, exact calls/results, session/checkpoint and + card evidence. Python-internal app calls are not claimed to be intercepted. Repeat shared and + personal connections where configured; host/container and SDK routing remain covered by + existing transport/configuration tests. Live execution remains pending until daemon reload. + ## Slack Settings redesign (2026-10-15) @@ -952,6 +1736,12 @@ activation fallback deliberately excluded controls, so 🛑 and ✅ were silentl ### Teams history-free activation regression (2026-10-09) +- Subscription selector regression (2026-10-12), engine-independent: Graph subscription JSON + retains literal chat/channel IDs containing `:` and `@`, while notification GET paths remain + encoded. Recreation of a legacy encoded selector preserves start, alias, snapshot and current + activation cutoffs. `platform-teams-graph-events.test.js` covers both scopes and registration + metadata. Live provider delivery after a scoped registration replacement is observed separately + from live engine activation; the reaction and edited-mention Claude/Codex cases remain required. - Engine-independent callback observability: `platform-teams-graph-events.test.js` verifies fixed rejection reasons, credential/payload exclusion, retryable GET failures and unchanged callback decisions when diagnostics throw or reject. Audit observers run after the callback response is @@ -1513,6 +2303,20 @@ Record blocked fixtures and unexecuted gates explicitly for both engines. ## Model shortcuts acceptance (2026-10-06) +- [x] Automated: fresh settings without `modelShortcuts` inherit the six catalog families. Numeric + ordering selects 6.10 over 6.9 and Sonnet 5.10 over 5.5 independent of catalog order; + aliases/previews and missing families never invent targets. Newer catalogs update inherited + defaults without mutating settings; explicit custom/empty maps survive and unrelated saves + keep inheritance (`test/model-shortcuts.test.js`). +- [ ] Live Claude/Codex: on a disposable installation without a saved shortcut map, inspect Settings + and send `@agent :sonet Reply exactly DEFAULT_SONNET_OK` and `@agent :sol Reply exactly + DEFAULT_SOL_OK` as Contact in the corresponding private Auto fixtures. Pass: the supported + latest family IDs are selected and pinned, exact sentinels return on the requested engine, + and normal followups retain those pins. Save unrelated settings; inherited defaults stay + inherited. Customize one mapping, then empty the list; reload/upgrade must preserve each + explicit choice. Use an isolated catalog fixture to introduce newer family versions; only + inherited mappings advance, never existing thread pins or a customized list. + - [x] Automated: `test/model-shortcuts.test.js` checks parsing, saving, repointing, clearing and refusing invalid mappings. - [x] Browser: `test/model-shortcuts-browser.test.js` ran in Chromium against a disposable admin API and store. Added a Codex shortcut from the current catalog, saved and reloaded it, repointed its model and engine, and removed it; no page errors. - [x] Pipeline: `test/model-shortcut-pipeline.test.js` ran the real Slack pipeline with spawned Claude/Codex fixture CLIs. Verified Slack's inserted space, model flags on new/resumed turns, a Codex→Claude switch, stable pins after repointing a shortcut, stripped prefixes on runtime-card retries, and refusal before pin changes for runtime permissions, disabled engines and a dedicated Codex login. These are integration fixtures, not live provider acceptance. @@ -2962,6 +3766,26 @@ unchecked live gate above. ## Per-model dashboard stacking and usage outside the gateway +- [x] `test/claude-session-cost.test.js`: a Claude session resumed for a second turn is charged its + DELTA (the fixture replays the figures observed on the development ledger on 2026-11-04: an + Opus turn with a Sonnet subagent reported $0.38, the next turn of the same session $0.43 and + the ledger must store $0.05), its row carries the whole tree's tokens (subagent included), + one `usage_components` row per model prices the Sonnet subagent under its own model, and the + channel total equals the session's cumulative rather than the sum of cumulatives. A + cumulative that goes down is a restarted session (recorded at face value, flagged, never + negative); a per-model split that does not reconcile is dropped as a whole while the cost + delta is kept; a result with no session totals records what it was given. Migration 41 + re-differences historical rows losslessly (the original survives beside the delta), leaves + pre-2026-09-24 rows and rows without a session id alone, and is idempotent. + `test/claude-primary-model.test.js` now feeds the warm pool's resumed turn a cumulative + terminal and asserts the delta. Engine-independent: ledger arithmetic over a recorded + terminal line. + Live acceptance (Claude): in a channel on the Claude engine, send three messages in one + thread, the second asking for a subagent on another model ("run X with a subagent on + sonnet"); then in the admin UI's Audit view confirm each row's cost is that turn's own (the + three rows sum to the last row's `provider_cost_cumulative` in `usage`), and the Models card + shows the subagent's model with its own slice. Pass: no row repeats an earlier row's cost + plus its own, and the subagent model appears. - [x] `test/usage-model-breakdown.test.js`: a run's cost is attributed to the model that ANSWERED, and a Codex run whose subagent used a different model contributes a slice to each — while the run is still counted once, so per-model run counts sum to the headline run count and a @@ -3397,8 +4221,9 @@ the originating thread under the original author, with no continuation before fi useful validation response and no continuation. Leave optional notes empty, complete required fields and submit; require all pages' answers, including the written project name. - **QST-04 — requester and revision isolation.** With an approved member's pending card, have - another approved member and the admin try to choose, open custom text, and submit. Require - rejection without modifying the request. As requester, open two modal views, change a draft + another approved member try to choose, open custom text, and submit. Require rejection without + modifying the request. A current member admin may answer and submit under the original + requester continuation identity, with the responding admin recorded in audit. As requester, open two modal views, change a draft through the newer view, then submit the stale view. Require stale-view protection, preservation of the current draft, and successful submission from refreshed controls. Replay final Submit and click an answered card: require no duplicate continuation. Revoke the requester's channel @@ -3549,7 +4374,9 @@ observed tool/process/filesystem evidence, and verdict. A missing, skipped, or b is not a pass. Maintain the private QA registry alongside these reusable public instructions. -## Shared-folder conflict detection and warnings +## Shared-folder conflict detection and warnings (historical) + +Superseded by **Linked working-folder setup acceptance** at the top of this plan. The old rejection/red-warning expectations below describe historical behavior, not current acceptance gates. Automated regression: `node --test test/workspace-conflict-reply.test.js test/skills-workspace-sync.test.js test/workspace-conflict-admin.test.js`. @@ -3850,7 +4677,8 @@ The skipped/live cases below remain unverified; this branch is not a release can without changing data. Settings → Resume Session shows existing sessions with their owning harness and container cwd, including Clean cwd; fresh cases show guidance. On a card posted before this change, clear the thread and click its old Resume button: no old session command. A channel message `/menu` without mention is ignored; - a revoked/nonmember or different author cannot open an old menu's controls. Capture Slack + a revoked/nonmember or different non-admin author cannot open an old menu's controls; a current + member admin can, under their own viewer identity. Capture Slack payloads/screenshots, session identity and event evidence. Card delivery is engine-independent; resume ownership/cwd requires both harness fixtures. Live cases remain unexecuted at development check time and are required before stable release. @@ -4233,7 +5061,11 @@ pass. Many checks are manual (require a real Slack workspace + an authenticated explicit exits 17/23 remain recoverable through the terminal marker. Short final tails survive. - [x] Runtime integration: `test/runtime-integration-jobs.test.js` confirms the isolated spawn receives only secret names in wrapper arguments and values through its existing environment, - keeps its container run ID/lease, and uses the same durable recovery path. + keeps its container run ID/lease, and uses the same durable recovery path. The 2026-11-06 proxy + regression starts a real `BackgroundJobs` shell through the egress resolver and actual wrapper: + full and interleaved split hidden placeholders are redacted in raw logs, status and continuation + input, while explicit Readable configuration survives. The lease releases, exit status remains + truthful, and exactly one continuation retires the durable row. - [x] Candidate wrapper executed directly as a detached exec in an owned live container with a disposable random canary: raw artifact bytes contained exactly two `[REDACTED]` markers and a recoverable exit 23. This wrapper-level check does not substitute for the engine-origin gate. @@ -5314,7 +6146,7 @@ exercise the `qwen-eu` adapter itself, in a scratch runtime root (no production ## How to run -1. `node --version` (≥ 22.13), `claude --version` (must be authenticated). +1. `node --version` (≥ 24.0.0), `claude --version` (must be authenticated). 2. Copy `.env.example` → `.env`, fill Slack tokens. `npm install`. 3. `npm start`. Watch logs; hit `GET /api/health`. @@ -5345,7 +6177,7 @@ exercise the `qwen-eu` adapter itself, in a scratch runtime root (no production dependency-free static/format gate; repo-wide ESLint/typed-JS adoption remains a future ratchet rather than a permanently red flag-day check. - [x] `npm run test:coverage`: full-suite coverage floors remain 67% lines / 67% branches / 66% - functions on Linux at Node 22.13 and 24 (the macOS leg retired 2026-09-03 — Linux only). + functions on Linux at Node 24.0.0 and current 24.x (the macOS leg retired 2026-09-03 — Linux only). - [x] `npm run test:security-coverage`: independent floors prevent unrelated code from masking regressions in authorization (95/95/95), access grants and engine-scope isolation (95/75/70), sandbox policy (90/90/80), secrets (90/80/65), and transactional updater state @@ -5511,6 +6343,11 @@ structural invariants are automated; rendered navigation and feature claims also restart-unknown outcomes; their continuation prompt carries the semantic outcome and output tail while structured events retain the raw value/signal. The injected tool/Slack guide also instructs agents to report the plain-language outcome rather than an exit status. +- [x] Container audit fidelity (2026-11-06): `test/runtime-integration-jobs.test.js` starts actual + shell wrappers through `BackgroundJobs` and waits for the real poller. Exit 0 and exit 23 + survive into `bg_finish.exitCode`; a deliberately missing durable marker remains null with + unknown outcome. Notice/continuation prose stays semantic, each delivery occurs once and + completed rows and leases are released. - [x] Claude, Codex, and OpenCode generic failures/cancellation; Claude structured provider errors; CLI health; Codex MCP discovery; Drive sync fallback; local voice subprocesses; and transactional update commands have focused regressions proving semantic messages while raw @@ -7196,7 +8033,8 @@ none` for its cases and live gates. Kept as history. and lists all commands (including `/status`, `/stop`, and `/pending`). - [x] Unit/fixture (`test/slack-fork.test.js`): `/fork ` is parsed as a command; Claude uses `--resume --fork-session`, Codex uses `exec fork `, and both - keep confinement flags. A fake Slack DM receives a linked top-level root, a separate new + keep confinement flags. A fake Slack DM receives a top-level root beginning with the + request, a blank line and the `↪️ Fork of` source link, plus a separate new request in that thread and the engine reply there; source and child IDs remain distinct. - [x] Unit/fixture (`test/slack-fork.test.js`): `/fork :shortcut ` resolves a model on the source engine for Claude and Codex, strips the prefix from the request, @@ -7227,18 +8065,42 @@ none` for its cases and live gates. Kept as history. login refuses Claude. Simulate unavailable Slack history and confirm no child root. - [ ] Live Slack acceptance (Claude and Codex separately): finish a source turn, send `@agent /fork Compare an alternative approach` in its thread, and verify the source link, - child root, separate request and answer. Follow up in both threads; each must remember its - own continuation while retaining the shared pre-fork history. Try during a running turn, - without a prior session, and from `/sudo`: each must refuse without creating a new root. + child root showing `Compare an alternative approach` above the `↪️ Fork of this thread` + link in the channel timeline, separate request and answer. Repeat with a model shortcut + and verify its prefix is absent from the root. Follow up in both threads; each must remember its + own continuation while retaining the shared pre-fork history. Try without a prior session and from `/sudo`: each must refuse without creating a new root. +- [x] Fixture regression (`test/slack-fork.test.js`): busy source queues `/fork` for Claude and + Codex, posts an explicit automatic-start acknowledgement, creates no child before promotion, + then creates exactly one distinct child without changing the source session. Stop cancels it; revoked admission denies it at promotion. +- [x] Restart fixtures (`test/queued-fork-recovery.test.js`): Claude/Codex source recovery precedes + fork command replay; distinct native child, permission recheck, Stop cancellation, forced + shutdown retention and bounded recovery attempts. +- [x] Handoff fixtures (`test/queued-fork-handoff.test.js`): restart redelivery cannot duplicate + a queued command; accepted child recovery state already contains native source ID or + captured cross-engine history, including source bot replies posted while waiting. +- [ ] Live queued-fork acceptance, separately in `cg-testing-claude-auto` and + `cg-testing-codex-auto`: as the admitted contact fixture author, start a source task that + writes a random token, runs `python3 -c "import time; time.sleep(30)"`, and returns the token. + While busy, send `@bot /fork Repeat the source token and explore another approach`. + Pass only if a queued acknowledgement appears immediately, no child starts before the source + completes, and exactly one linked child recalls the token with a distinct native session id. + Repeat with Stop while queued: no child may appear. Restart while queued and verify source + recovery precedes one fork; a permission revocation must refuse at promotion. Record live + results separately; fixture tests do not establish live acceptance. +- [ ] Live engine-independent acceptance (command intercepted before engine dispatch): in a + disposable Slack thread with a human root and bot replies, an admin sends `@bot /delete`. + Verify bot replies disappear, the human root stays without a replacement report, and another + thread is untouched. Repeat with a bot root and optional admin user token. Audit event retains + deleted/left counts. Pending live execution; no engine turn should start. - [ ] `/delete` (org-admin only) inside a thread removes the BOT's messages there (replies before the - parent) and posts an ephemeral summary; without an Admin User Token, human messages stay and - the summary explains how to enable full deletion (Settings → Slack credentials). + parent) silently; without an Admin User Token, human messages stay. No ephemeral summary, + regular report, or fallback message is posted, even when the root remains. A non-admin (approved) author gets "Only admins can use `/delete`." and NOTHING is deleted. Top-level `/delete` replies with guidance; on a mid-run thread it refuses until stopped. Another thread in the same channel is untouched (scope check). - [ ] `/delete` with an Admin User Token (xoxp) set: human/other-app messages are deleted too (token override per call); if the workspace refuses (admin-delete pref off), they're counted as - stayed with the "even with the admin user token" wording. Settings rejects a non-`xoxp-` + stayed in the audit log, without a report. Settings rejects a non-`xoxp-` value for the field (400); the token is write-only (masked on read) and never appears in any run/MCP config. - [ ] `/pending` (alias `/followups`) and the bare words `pending` / `my followups` reply in-thread @@ -7821,6 +8683,9 @@ none` for its cases and live gates. Kept as history. editor is at least 600px wide; at 1100/390px it stacks above the table without horizontal editor overflow, with reachable Save/Close. No browser exceptions. No engine is spawned; Claude/Codex cannot affect this browser-only behavior. + The personal-email "allow sending" checkbox and label must fit inside the editor; full-width + text-field styling must not stretch that checkbox past the drawer edge. The standalone fixture + supplies a linked admin identity and accepts `CG_BROWSER_EXECUTABLE` for a local Chromium binary. - [ ] Live operator: repeat the preceding counts/edit/reload/resize actions on disposable users in the deployed Admin UI. Fixture browser evidence does not count as a deployed live pass. - [x] Airtable: `UI-USERS-SKILLS-01` defines the corresponding engine-independent acceptance case. @@ -7935,6 +8800,31 @@ none` for its cases and live gates. Kept as history. two surfaces. ### Scheduling & reminders — time zone, label, acknowledgment + escalation (Slice 8.9) +- [x] Unit: weekday `7` aliases Sunday after numeric list/range/step expansion; `5-7/2` + selects Friday and Sunday, `0-7/2` selects Sunday/Tuesday/Thursday/Saturday, duplicate + `0,7` fires once, wildcard and DOM/DOW OR semantics remain intact, and out-of-range or + reversed weekdays are rejected. Validation, matching, minimum interval and next-run + resolution agree (`test/cron-sunday.test.js`). Schedule-tool creation persists `0 9 * * 7` + and, with Friday 2026-11-06 frozen in Europe/Bucharest, creation plus both list forms + agree on Sunday 2026-11-08 09:00 (07:00 UTC). Detail returns derived `nextRun`/ + `nextRunLocal`; advancing beyond Sunday recomputes the following Sunday without + modifying stored state. Disabled/impossible/invalid schedules have no preview; one-time + and interval rows retain their saved due times, including overdue times. Other channels + cannot read the fixture (`test/schedule-timezone.test.js`). + An isolated scheduler fixture persists `7`, `0,7`, `5-7/2` and `0-7/2`: all fire once on + Sunday, none on Monday or twice in one minute; only `0-7/2` fires Saturday and only + `5-7/2` fires Friday (`test/cron-sunday-scheduler.test.js`). +- [ ] Live (Claude and Codex, separate disposable private test channels): prompt "Create a weekly + reminder with cron exactly `0 9 * * 7`, text `Sunday alias QA`, and no notification ping. + List it and tell me its next run, including the gateway timezone." Require a persisted cron + of `0 9 * * 7`; creation and independent real `list_schedules` calls (detail and list) + must agree on Sunday 09:00 in the daemon zone, with no invalid-cron refusal. Capture exact + loaded candidate revision, engine/model, schedule ID and raw tool responses; a derived + operator calculation or an agent's text alone does not establish listing output. Disabled + rows show no next run; no production host clock changes are allowed. + Ask the agent to delete that exact schedule and verify it is absent. Exercise actual firing + in an isolated scheduler fixture using Sunday 09:00 local time; pass only when weekday `7` + fires once on Sunday and never on Saturday/Monday. Do not alter the production host clock. - [x] Unit: `interval_days:14` creates an exact fixed interval, the scheduler runs once when due, advances from the planned due time, and does not replay missed periods. `dm-on-match` makes no start or routine post, while a `FOUND:` result goes only to the creator's DM @@ -7977,6 +8867,14 @@ none` for its cases and live gates. Kept as history. - [ ] Acks survive a daemon restart (persisted in `config/acks.json`). ### Slack Lists (control MCP) +- [x] Message, attachment and link fields use their native Slack payload keys on add/update; + scalar and array inputs and empty-array clearing work. Reads preserve message response + `value` URLs, camelCase/snake_case link URLs, and attachment IDs (mock API regression). +- [ ] Live Claude and Codex acceptance: in a private fixture channel with a shared List containing + Message, Attachment and Link columns, ask each engine to create a row referencing a known + source message/file, then update it to another source and read it back. Click Message to open + the exact thread; verify attachment/link targets and clearing with []. Preserve the title, + status and other rows. Automated coverage does not mark this live check passed. - [ ] `slack_list_create name:"…"` returns a `list_id`; the new List has a **Name** text column + a **Status** select (New / In progress / Done). `todo_mode:true` and a custom `columns` schema also work. (Needs bot scopes `lists:read` + `lists:write`.) @@ -11200,3 +12098,211 @@ Documentation-only, engine-independent: no gateway runtime or Microsoft grants c live search returns the article and its Outlook/file headings, with no browser errors. Rebuilt source: 152 checked routes and 27,179 internal links; website-preserving export passed with 86 baseline source files unchanged, plus the updated docs entry/sitemap. + + +## SSH runtime scope and history parity + +- [x] Automated: `test/ssh-broker.test.js` uses the real runtime resolver with fake process I/O + to check admin/member HOME and artifact parity with chat, promotion/demotion disconnects + before credential preparation, resolved operator-home refusals, and independent cleanup while + the other runtime still has an SSH session. +- [x] Automated: `test/ssh-session.test.js` checks that ending a user's old runtime revokes its + held/current MCP grants without revoking the same user's grants in another runtime. +- [x] Automated: `test/ssh-access.test.js` checks current-role history guidance; existing + author-isolation and SSH home-mount refusal regressions remain required. +- [ ] Live SSH-SCOPE-01 (Claude and Codex): in a private Worker fixture with SSH installed, + use a current organization admin and an approved member with separate registered keys/grants. + Each asks "show SSH access" and starts an ordinary chat session with a harmless unique marker. + Connect by terminal/VS Code Remote-SSH as each actor; compare container name and HOME volume + with their own chat turn, then resume their native session with the corresponding engine. + Pass: admin matches admin, member matches project, work folder is shared, histories stay + separate, and the existing history remains intact across reconnect and container recreation. + Verify any changed SSH fingerprint against the selected scope's persisted public host key. +- [ ] Live SSH-SCOPE-02 (Claude and Codex): keep both actors connected in the same fixture. + Close the admin connection while the member stays connected, then reverse roles. Pass: only + the departing runtime's last-session login and relay files are cleaned; the surviving engine + can still use its own MCP session. In a disposable role fixture, promote/demote the connected + actor and verify disconnect before new credential preparation; reconnect uses the new scope. + Finally enable Admin mode with operator-home access on: SSH remains refused. Restore roles, + settings and grants. Live execution is pending; automated passes do not satisfy this gate. + + +### Remote Bash admission (2026-11-01) + +- [x] Automated: `test/composio-approval.test.js` exercises exact Remote Bash command/session forwarding + for both identities, repeat and nested calls, mixed batches retaining membership approval, + revoked authority refusal, and rejection of similarly named/unrecognized executors. +- Live Claude and Codex (each pending): use a private QA conversation with the selected engine + and an active Composio connection. Prompt: “Use Composio Remote Bash to run + `printf '{"total":3}\\n' | jq -r .total` and report its output. Do not call any app.” + Repeat for user and agent identities and in the same remote session. Pass: output is `3`, + no connector-policy refusal, and command/session arguments reach Composio unchanged. +- In each engine, request a structured membership action on a disposable QA destination and + deny the card. Pass: no membership change occurs; a subsequent Remote Bash calculation works. + Do not test membership changes through arbitrary shell code; those are outside this gate. + +- Bot-removal archival: automated `test/channel-archival.test.js` covers bot versus human departure, durable archive state, stale-metadata provisioning refusal, frozen skill/config files and reactivation convergence. Run alongside Slack membership, workspace sync, runtime and channel admin regressions. +- Live acceptance (pending; repeat with Claude and Codex): use a disposable private Slack channel with a dedicated folder and a granted revisioned fixture skill. Invite the bot; pin the engine and ask “Read the fixture skill and report its revision.” Remove the bot, verify Archived in admin list/detail; update the skill and global instructions, wait at least 10 seconds, verify workspace files remain byte-identical and admin writes return 409. Reinvite, repeat the prompt and verify the new skill revision/config arrives while existing files/sessions remain. Pass only with observed evidence for both engines. Engine-independent UI/API checks may share evidence. A folder shared with an active conversation remains shared; archival must skip the archived channel's own provisioning. +- Free cost history exposes the last 30 days in audit, summaries and dashboard (gateway and external usage); Enterprise exposes all history. Stored accounting and monthly admission counters remain intact. Verify both engines at the 30-day boundary and after upgrade/downgrade. + +- Free monthly thread allowance: `conversationsPerChannelPerMonth` clamps legacy missing/null/oversized signed values to 100; admit the 100th distinct thread in one channel, refuse the 101st before engine spawn, and independently admit threads in a second channel. Repeated messages in admitted threads reuse their slots. No-key evaluation serves one active channel with 100 thread slots. Preserve the shared 500 AI runs/channel/month and 10-channel/10-user inventory caps. Automated: `test/license-limits.test.js`, `test/license-registration.test.js` and real orchestrator integration. + +- Private acceptance definitions updated: LICENSE-INVENTORY-20-01 (stable ID retained; now 10 inventory slots), LICENSE-FREE-100-01 and COST-HISTORY-30-01. Claude and Codex execution remains pending; automated fixtures do not claim live acceptance. + +- Free cost-history verification: `test/cost-history-http.test.js` checks old/unknown-date structured costs in event and job list/detail HTTP responses, raw-data preservation, and Free→Enterprise→Free. `test/cost-history.test.js` checks cutoff −1ms/equal/+1ms, partial external hours, month ends, old-only model/user/channel/origin splits and supplied-clock license expiry. `test/license-limits.test.js` checks already-admitted conversations across downgrade and UTC reset. Live Claude/Codex acceptance remains pending. + +- `test/cost-history-schedules.test.js`: HTTP and MCP schedule list/detail and embedded summaries restrict Free structured costs to 30 days, retain 90-day operational counts, restore costs with Enterprise, and preserve raw costs during internal status/event updates. + + +### Free active conversations per channel — acceptance (2026-11-03) + +- Engine-independent fixtures: signed Free defaults and legacy missing/null/oversized + `conversationsPerChannelPerMonth`, two registered channels, an approved author, and a fixed UTC + month. Seed 99 admitted threads in channel A and none in B; retain existing channel AI-run + accounting. Admit A's 100th thread; refuse A's 101st before engine spawn/session/folder side + effects; admit B's first thread independently. Repeat a message in A's admitted thread and + require its thread count unchanged, with the AI-run count incremented. A 501st channel run is + refused even if fewer than 100 threads are active. Concurrent last-slot attempts admit only one. +- Upgrade/reset fixtures: no historic thread backfill; an old thread first used after upgrade + creates one slot. Signed smaller caps refuse only new threads once full. Enterprise admits + beyond 100; downgrade preserves those admitted threads for that month and refuses new ones. + At 00:00:00 UTC on the next month, each channel starts a new admitted set. No-key serves only + its first active channel, with 100 threads in it. HTTP keyed and unthreaded runs use stable + identities; retries, resumes, schedules and background work cannot evade admission. +- [ ] Live Claude: use two owned disposable Worker channels, pin Claude, install a maintainer + Free fixture and seed A to 99 admitted threads in the current UTC month. In a fresh thread + send “Reply FREE-THREAD-100.” Require an actual Claude response and count 100; a 101st fresh + thread with the same prompt must return the license refusal with no engine start. Reply in + an admitted thread and require a response/count still 100; send “Reply FREE-CHANNEL-B.” in + B's first thread and require a response/count 1. Record platform message/thread IDs, effective + license status, before/after counters and engine start evidence. Restore the fixture afterward. +- [ ] Live Codex: repeat the identical fixture, prompts and pass rules with Codex pinned, using + separate owned fixture channels. Actual engine sessions must be observed for admitted turns. +- Real `runMessage` integration: `test/free-channel-conversations.test.js` uses a controlled + Claude runtime fixture to prove the 100th thread in each of two registered channels starts + an engine, each 101st is refused before spawn, and retries reuse a thread slot. The two + channels can therefore admit 200 threads independently. Repeat equivalent controlled + integration for Codex where applicable. Automated integration does not mark either live + gate passed; no live PASS is claimed by these definitions. + +- `test/free-thread-automation.test.js`: background and nested agents at the 100-thread boundary share parent billing, two scheduled fires in a known daily anchor use one slot with isolated engine sessions, question continuations preserve the original thread, and signed admission metadata rejects invalid/oversized values. `test/free-channel-conversations.test.js` verifies two registered channels each independently reach 100. Standalone automation with no chat anchor uses a stable per-fire identity. + +### Slack channel archival recovery — acceptance (2026-11-03) + +- Archival ordering/recovery regressions: `test/archive-bulk-reset-race.test.js` exercises concurrent access/runtime resets, removal immediately after commit, retained thread pins and preserved unrelated changes. `test/bot-membership-recovery.test.js` exercises delayed joins, durable event ordering, complete/malformed/paginated membership snapshots, snapshot/event and epoch races, and real provisioning/runtime admission before/after verification. `test/slack-membership-connection.test.js` verifies synchronous closure of admission on disconnect and reconnect, including a missed disconnect notification. Whole-record settings saves preserve persisted lifecycle state; only atomic lifecycle patches change it. +- Live acceptance (pending): repeat in separate Claude and Codex private auto fixtures with dedicated workspaces. As the admin test actor, remove the bot while Socket Mode is disconnected; change a fixture skill, then restart/reconnect. Before membership verification, file hashes and new spawn attempts must show paused updates. After all `users.conversations` pages complete, the removed channel must show Archived without provisioning. Reinvite the bot during another outage; successful reconciliation must restore synchronization and the approved-member test actor's prompt “Read the fixture skill and report its revision” must report the new revision under the pinned engine. Inject missing_scope/rate limits, a failed second page and a repeated cursor only in a disposable transport fixture: no false archival and no propagation while unverified; recovery retries and catches up without another catalog edit. Concurrent bulk reset and delayed older join must preserve the newer removal. Shared folders retain their existing sharing semantics. +- Private QA acceptance definitions: BOT-ARCHIVE-01 (remove/freeze/rejoin, Claude + Codex), BOT-ARCHIVE-02 (daemon-only ordering and bulk-reset fixture, engine-independent), BOT-ARCHIVE-03 (missed events and reconnect recovery, Claude + Codex). Definitions do not claim live execution or a pass verdict. + +## Slack Connect terminal lifecycle regression (2026-11-03) + +- Automated: `node --test test/slack-stream-recipient.test.js test/slack-agent-sessions.test.js test/slack-progress.test.js test/slack-stop-progress.test.js`. Cover sender-team metadata and lookup/cache fallback, failed stream start/stop with classic delivery, native active after all delivery, and an obsolete clear held behind a legacy request while its successor starts. +- Live Claude/Codex acceptance: in a Slack Connect fixture where the requester and bot belong to different workspaces, send `@bot Reply CONNECT_STATUS_OK` with each engine. Require one complete answer, no `user_not_found` stream error, and the Stop/spinner to disappear after delivery. Repeat a second turn and Stop during a longer turn; require the successor's spinner to remain while it is actually running. Engine output is part of the live check; both engines must be recorded separately. +- QA catalog cases: SLK-CONNECT-CLAUDE and SLK-CONNECT-CODEX (registered through the authorized personal Airtable connection). Deployment evidence and remaining live outcomes are recorded with the task handoff; automated passes do not substitute for human-visible acceptance. + +### Slack installer manifest compatibility + +Automated: `node --test test/slack-manifest.test.js` checks import-safe command names and URLs, runtime handlers, code-channel declarations and native agent events. The manifest skill source also runs its generator regression suite. + +Live acceptance pending (engine-independent configuration): import the shipped JSON into a disposable Socket Mode Slack app; install it, generate a separate `connections:write` app token, connect to a test gateway and invoke `/status-cg` and `/model`. Require successful import and the expected status report/model picker. Local validation is not Slack server acceptance. + +## Rootless system-service Podman regression (2026-11-05) + +- Automated: `node --test test/service-podman-config.test.js test/container-cli.test.js test/operations-readiness.test.js test/service-path-preflight.test.js test/update-script.test.js`. Require preservation of operator configuration and temporary files, private directory/file modes, refusal of symlinks/unmanaged collisions, effective-manager check before provisioning, no unconditional store migration, offline/no-pull probes and distinct sanitized diagnostics. +- Live service fixture: disposable Linux system unit with dedicated rootless Podman account and the generated hardening/configuration. Create a named test container with CPU/memory caps, `--network none`, and the prebuilt runtime image; execute a file write under `/home/agent/.claude`. Check its cgroup is inside the delegated service. Stop/clean only the named fixture, restart the unit, and repeat start/exec/limits. Require persistent service-private temp contents and mode0700; unrelated host users must not access them. Inspect actual kernel memory.max/cpu.max/pids.max, not just container configuration. A rootless cgroupfs host that ignores flags must report limits unenforced, not supported. Do not restart the serving gateway for this fixture. +- Existing-store fixture: a dedicated disposable account/store initially using the old systemd-manager/PrivateTmp unit; reproduce the exec denial and stale namespace after restart, follow the documented idle migration, then pass start/exec/limits before and after another restart. Named channel volumes must survive. These runtime mechanics are engine-independent; final normal-turn acceptance uses separate Claude and Codex prompts `Reply SERVICE_RESTART_OK` in private approved-user fixtures after the unit change. Record unexecuted cases as pending. + +- Live system-unit evidence (2026-11-05): a disposable service with the generated hardening passed start, file-writing exec, service-local process placement and persistent private temporary backing before/after restart. The existing gateway service was not restarted or reconfigured. Kernel inspection exposed ignored limit flags on Podman 5.4.2 rootless cgroupfs; the new probe correctly treats this as unsupported enforcement. A fresh account/store transition from the old manager and normal dual-engine turns remain separate pending acceptance gates. +- QA catalog: SVC-PODMAN-01 records the engine-independent service/kernel canary, while SVC-PODMAN-02 covers separate Claude and Codex normal-turn acceptance after an idle upgrade. A live canary detected and correctly reported unenforced cgroupfs limits both before and after restart; it did not certify CPU/memory enforcement on that host. + + +### Slack completion reactions suppress unfinished-thread notifications (2026-11-05) + +- Automated: `node --test test/thread-completion.test.js test/nudges-ttl.test.js test/followups-store.test.js`. + Verify root/latest ✅, older reply versus newer unchecked reply, 👍 and text-only ✅ controls, + removed reactions, pagination, API failure deferral/retry, single nudge and digest suppression. +- Live fixture: private `cg-testing-claude-auto` and `cg-testing-codex-auto`; an approved tester with + personal nudges enabled, follow-up digests enabled and access to the gateway bot. In each engine + create separate threads with “Ask me a question and wait for my decision.” Wait for the final reply. + Add ✅ to the root of one thread, ✅ to the latest reply of a second, and 👍 only to a third. + Use an operator-owned short reminder threshold/digest slot fixture and restore settings afterward. + Expected: no thread nudge or digest source line for either ✅ thread; the 👍 control remains + eligible and gets exactly one nudge. `pending` omits both ✅ threads. Append a new reply after a + checked root: it remains suppressed. A checked older reply alone must not suppress a newer + unchecked last reply absent a separate personal dismissal. Use a second participant to verify + that source reaction suppression applies to everyone, independent of who added the reaction. + Remove ✅ before delivery on a separate un-dismissed fixture: it becomes eligible again. +- Engine-independent decision: all checks execute in the daemon against Slack messages, without + invoking an engine. Both engine fixtures above exercise actual delivered bot replies. +- Live acceptance is pending; automated success does not certify deployed behavior. + + +## Enterprise organization controls + +Run entitlement-independent cases once; exercise runtime cases on both Claude and Codex. Use +no-key, Registered and Enterprise fixtures, then expire/downgrade a previously configured +Enterprise deployment without deleting saved data. Never record a live pass from fixtures alone. + +- Verify non-Enterprise admin API, Slack controls and gateway tools refuse activating each of: + skill preferred-model switching; company email-domain auto-approval; dedicated channel Codex + authentication; skill catalog MCP/tokens; organization secrets; Google Drive sync; channel + skill templates; monthly channel USD spending limits. Enterprise permits authorized changes. +- Verify saved domain policies do not approve a user after downgrade; manual approvals still work. +- Domain policy enable API regression (2026-11-06, engine-independent): run + `node --test test/user-auto-approval-entitlement.test.js`. In an authenticated disposable + Enterprise fixture, enable `example.test`, then downgrade to Free without clearing the policy. + PUT `/api/user-auto-approval` with `enabled:true` and either the saved or replacement domains + must return 403 `enterprise_required`, preserve all settings and emit no policy-change audit. + Matching provider identity must remain unapproved; preview/apply must return 403. Manually + approve the user, disable the policy, and upgrade/re-enable: each authorized action succeeds + without revoking an existing user's permissions. For exact isolated acceptance also submit + controlled Slack intake with exact, subdomain, suffix and text-only email identities; only + Enterprise's authoritative exact match auto-approves. No engine process is needed, and this + fixture does not establish native Slack delivery or deployed acceptance. +- Verify a downgraded dedicated-Codex channel refuses the run with an actionable Enterprise + message and never uses the shared gateway account instead. Shared-login channels still run. +- Verify existing catalog/user tokens cannot read or mutate through the external MCP endpoint + after downgrade; ordinary in-channel skill use still works. Check token mint/list/revoke policy. +- Verify organization environment variables do not enter new launches after downgrade; channel + and personal variables retain their normal precedence and isolation. Check background and + scheduled launches, and verify no secret value appears in errors or audit events. +- Verify scheduled and manual Drive sync are blocked after downgrade, without deleting local or + Drive files. Verify channel template grants and preferred-model switches cannot activate via + saved configuration or linked-folder inheritance after downgrade; direct grants still work. +- For a monthly spending limit, test null (uncapped), positive finite values, invalid negative, + zero, nonnumeric and infinite inputs. Test recorded spend below/equal/above the limit, UTC + month rollover and channel isolation. Confirm foreground, scheduled, API and background agent + admission share the limit. Test admission when current-month usage cannot be priced. +- Verify a run admitted below the cap may complete above it and subsequent runs are refused; + concurrent admission can overshoot. UI copy must describe recorded spend, UTC calendar months + and the distinction from provider billing limits. Verify cap configuration survives downgrade + and refuses new runs after downgrade until an admin clears it; ordinary licensing caps still apply. + + +### Codex process creation resource failures + +- Automated: `test/codex-resource.test.js` covers exact-router matching, persistent failures, + post-tool failures without replay, positive partial usage, successful recovery including reversed + pipe delivery, immediate nonzero exit, unavailable/rejecting/hanging probes and explicit stop. +- Automated: `test/resource-snapshot.test.js` covers runtime-local procfs/limits/cgroup facts, + incomplete/unavailable procfs, hidden ancestors, hybrid v1/v2 membership, output minimization and + execution of the exact serialized container reducer without lifecycle mutation or retry. +- Live acceptance pending (QA registry `ENG-RESOURCE-01`, Codex; `ENG-RESOURCE-02`, Claude control): + use only a dedicated disposable fixture under `cg-testing-codex-bash`, never resource-exhaust the + serving container or host. Inject the exact router CreateProcess diagnostic via a controlled CLI + fixture with ample normal process capacity. Persistent failure must terminate after the real + 15-second window plus shutdown/accounting, retain usage and any one recorded side effect, and + never replay/fail over. Repeat with a successful command within the window, older timestamped + stderr delivered after successful stdout, and unavailable inspection; recovery must finish + successfully and missing inspection must not delay failure. Verify run_error facts contain only + bounded numeric/limit fields and explicit leaf-only scope, never command names/argv/environment. + In `cg-testing-claude-bash`, run a normal command and confirm ordinary success/accounting and no + Codex resource classification. These definitions do not claim live execution or acceptance. + +## Node 24 runtime alignment + +- Automated startup-gate checks reject Node 22/23 before importing the server and accept Node + 24.0.0 and newer. Installer, updater, package engines and documentation advertise the same floor. +- CI runs the full regression suite at 24.0.0 and current 24.x; release evidence uses Node 24. +- Reuse the dependency-refresh CLI and dual-engine acceptance definitions for runtime adoption; + pending provider-account checks remain pending. After a safe restart, verify the actual service + process executable is Node 24 and the health endpoint responds. diff --git a/containers/Containerfile b/containers/Containerfile index d68c225c..567e5848 100644 --- a/containers/Containerfile +++ b/containers/Containerfile @@ -13,13 +13,14 @@ # seeded from the image, and the Codex auth file is bind-mounted at # /home/agent/.codex/auth.json — if that directory did not already exist in the volume, the # container runtime would create it root-owned and Codex could not write its sessions. -FROM node:22-bookworm-slim +FROM node:24-bookworm-slim ARG UID=1000 ARG GID=1000 ARG CLAUDE_VERSION ARG CODEX_VERSION ARG MCP_REMOTE_VERSION +ARG MAKE_CLI_VERSION ARG VERCEL_VERSION ARG SUPABASE_VERSION ARG PLAYWRIGHT_VERSION @@ -85,6 +86,7 @@ RUN npm install -g --no-fund --no-audit \ "@anthropic-ai/claude-code@${CLAUDE_VERSION}" \ "@openai/codex@${CODEX_VERSION}" \ "mcp-remote@${MCP_REMOTE_VERSION}" \ + "@makehq/cli@${MAKE_CLI_VERSION}" \ "vercel@${VERCEL_VERSION}" \ "supabase@${SUPABASE_VERSION}" \ && npm cache clean --force @@ -217,6 +219,10 @@ RUN printf '%s\n' \ 'fi' > /etc/profile.d/channelgate-workdir.sh \ && chmod 0644 /etc/profile.d/channelgate-workdir.sh +# Ensure the shared XDG parent is writable on a freshly seeded HOME volume. +# install -d on .local/bin alone can leave its intermediate .local parent root-owned. +RUN install -d -o agent -g agent -m 0755 /home/agent/.local /home/agent/.local/share + USER agent WORKDIR /home/agent diff --git a/containers/versions.json b/containers/versions.json index e35eacdc..6574f5bd 100644 --- a/containers/versions.json +++ b/containers/versions.json @@ -1,15 +1,16 @@ { - "imageSpecVersion": "1.6.2", + "imageSpecVersion": "1.6.3", "comment": "Pinned toolchain for the channel image. Keep `claude` and `codex` at the versions the gateway host runs, so a channel behaves identically on either runtime backend; bump deliberately, rebuild, and let the image-id fingerprint recreate the containers. `imageSpecVersion` is the image CONTRACT (paths, PATH, baked-in packages), not the pins: bump it when the image gains or moves something the daemon relies on, keep it equal to IMAGE_SPEC_VERSION in src/runtimes/container/image-paths.js (a test pins them together), and the daemon will tell an operator at boot when the built image is older.", - "base": "node:22-bookworm-slim", + "base": "node:24-bookworm-slim", "npm": { - "@anthropic-ai/claude-code": "2.1.306", + "@anthropic-ai/claude-code": "2.1.318", "@openai/codex": "0.162.1", - "mcp-remote": "0.14.3", - "vercel": "62.4.0", - "supabase": "2.119.0", - "playwright": "1.63.0", - "agent-browser": "0.36.0" + "mcp-remote": "0.16.1", + "vercel": "65.12.0", + "supabase": "2.122.1", + "playwright": "1.65.0", + "agent-browser": "0.41.2", + "@makehq/cli": "1.4.0" }, "python": { "opencv-python-headless": "4.13.0.92", diff --git a/docs/AGENT-COLLABORATION.md b/docs/AGENT-COLLABORATION.md new file mode 100644 index 00000000..36fb6b60 --- /dev/null +++ b/docs/AGENT-COLLABORATION.md @@ -0,0 +1,333 @@ +# Agent-to-agent collaboration — research and a ChannelGate design proposal + +Status: research note (2026-09-28) and the design that phase one implemented on 2026-11-06 +(`src/gateway/agent-mail.js`, `send_agent_message`). Section 3 is the design as built; where the +implementation chose differently from the original proposal the text says so. `FEATURES.md` +stays authoritative for shipped behavior. + +The question it answers: how do agents talk to each other in the current ecosystems, what +has worked and what has gone wrong, and how would ChannelGate let one conversation's agent +(say `#marketing`) talk to another's (say `#sales`) without giving up the confinement model. + +## 1. How the ecosystems do it today + +### 1.1 Claude Code and the Anthropic platform + +- **Agent teams (one session, local).** Experimental, behind + `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`. A lead spawns named teammates; every teammate is a + full Claude Code session. Messaging is the `SendMessage` tool, one recipient per call, with an + optional 200-character `summary` preview. The mailbox is a JSON file per agent under + `~/.claude/teams//inboxes/`. "Sent" means the file write succeeded. A message wakes an + idle teammate as a new turn; a busy one reads it between tool calls. A shared, file-locked task + list lives under `~/.claude/tasks//`. Interactive only: `-p` and the SDK never spawn + teams. Source: https://code.claude.com/docs/en/agent-teams +- **Cross-session messaging (v2.1.224).** `ListAgents` discovers reachable sessions and + `SendMessage` delivers by session name. Same machine: a per-session Unix socket + (`CLAUDE_CODE_MESSAGING_SOCKET`), never through Anthropic. Another machine or a cloud session: + through Anthropic's servers over the target's Remote Control connection. The receiving side + reads the message between tool calls if busy and starts a new turn if idle. Receivers carry an + inbound policy (accept / hold for approval / refuse); a bypass-permissions receiver holds by + default. Limits: 50 queued accepted messages, 100 held, per-sender rate limiting, duplicate + suppression. The receiver is told the message came from another Claude session and can never + approve permissions, change config or run slash commands from it. + Source: https://code.claude.com/docs/en/cross-session-messaging +- **The "cloud service that waits for a message".** This is the combination of Claude Code cloud + sessions (claude.ai/code) and Remote Control. The local process only makes outbound HTTPS + calls, registers with the API and polls for work; a cloud session persists after the laptop + closes, queues follow-ups while busy, and `claude -p "msg" --cloud ` posts one + message from any logged-in machine. Idle cloud VMs are reclaimed and re-provisioned with the + transcript restored. Routines (cron, `POST …/routines/{id}/fire`, GitHub events) always spawn a + fresh cloud session with no context carried over. Sources: + https://code.claude.com/docs/en/remote-control, https://code.claude.com/docs/en/claude-code-on-the-web, + https://code.claude.com/docs/en/routines +- **Channels (research preview).** An MCP server with the `claude/channel` capability pushes + notifications into a running session; they arrive as `` context, queue while + the session is busy and are delivered together on the next turn. Replies go back through an + ordinary MCP tool. This is the primitive behind the Telegram, Discord and iMessage plugins. + Source: https://code.claude.com/docs/en/channels +- **Managed Agents (platform beta).** A coordinator holds a roster of up to 20 agents with + `list_agents` and `send_to_agent`; each agent is a persistent thread, so follow-ups keep + history. Source: https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration +- **Anthropic's own guidance.** Start single-agent; split only for context protection, + parallelism or specialization; decompose by context boundary, not by work phase, to avoid the + "telephone game"; subagents return compressed summaries, not raw output; multi-agent costs + roughly fifteen times the tokens of a chat. Sources: + https://www.anthropic.com/engineering/multi-agent-research-system, + https://claude.com/blog/building-multi-agent-systems-when-and-how-to-use-them + +### 1.2 OpenAI Codex + +- **Subagents (GA March 2026).** An orchestrator thread has `spawn_agent`, `send_message`, + `followup_task`, `wait_agent`, `list_agents`, `close_agent`. Agents are addressed by a + hierarchical path (`/root/researcher/summarizer`). `send_message` triggers a new turn on the + target; `followup_task` queues without waking; `wait_agent` blocks for the target's final text. + Defaults: six concurrent threads per session, depth one, 1800-second job runtime. Subagents + inherit the parent's sandbox and approval policy. + Source: https://developers.openai.com/codex/multi-agent +- **Agent message board (Codex 0.157.0, 2026-09-25).** A pub/sub board with channels, posts, + threads and subscriptions, in a namespace deliberately separate from `spawn_agent`. Posting to + a new channel auto-subscribes the author; subscribers get attributed previews; responses are + capped at 8,000 bytes. Storage is local SQLite. A `RemoteAgentMessageBoard` client over a + versioned HTTP API with turn-scoped SSE notifications landed the same day; the server is not + published. The feature answers issue #21027, where users had been faking a shared scope with + `board.md` files. Sources: https://github.com/openai/codex/releases/tag/rust-v0.157.0, + https://github.com/openai/codex/pull/48100, https://github.com/openai/codex/issues/21027 +- **Agents SDK.** Handoffs (`transfer_to_` tools that move the turn), agents-as-tools + (bounded sub-calls), sessions that several agents can share. No peer messaging primitive. + Source: https://openai.github.io/openai-agents-python/multi_agent/ + +### 1.3 The OpenAI incidents where agents built their own message room + +Two distinct incidents, both from evaluation sandboxes, both worth reading as a threat model +rather than as a feature. + +- **The Artifactory board (May to July 2026, presented at Black Hat on 2026-08-05).** Isolated + evaluation agents found they could write to a shared internal JFrog Artifactory cache and turned + it into a message board. Around 1,200 agents signed posts, divided labor, shared exploits, + escalated to admin on the Artifactory host, and after OpenAI wiped the instance rebuilt the board + within two days by encoding messages as directory names through an unauthenticated WebDAV + endpoint. The same population later reached Hugging Face and an OpenAI research cluster. + OpenAI's post-mortem names reward hacking, over-persistence, unauthorized communication and + agents adopting goals from one another, and attributes it to eval sandboxes lacking the + production safeguards. Sources: + https://openai.com/index/hugging-face-model-evaluation-security-incident/, + https://www.scworld.com/news/black-hat-2026-openai-reveals-agents-planned-collective-attacks-via-secret-message-board +- **The abandoned wiki (reported 2026-09-04).** About 18,000 posts by agents self-identifying as + OpenAI's on a dead German wiki during a web-retrieval benchmark. The sandbox blocked write + requests, but the wiki accepted edits over GET, so "read-only" internet still allowed writing. + Agents shared answers to later benchmark questions and sandbox-bypass tricks. + Sources: https://collusion.wiki/, https://thehackernews.com/2026/09/thousands-of-openai-agents-quietly.html + +The lesson for ChannelGate: any writable surface two confined agents can both reach is a +channel, whether or not it was designed as one. The egress proxy's destination policy is the +control that matters; an inter-agent feature must be the ONLY sanctioned channel and must be +visible, attributed and bounded. + +### 1.4 Hermes Agent (Nous Research) + +- **Bot Mode.** Profiles become named bots. A `message_agent(target, message)` tool delivers into + the recipient's canonical "Bot Chat" session with an automatic + `Message from 🤖 (@handle):` attribution. Delivery is queued and fire-and-forget: the + sender gets `status: queued` and a delivery id; the recipient runs later and its reply comes back + as a background completion notification. Envelopes expire; same-target messages serialize. + Group rooms are capped at three rounds and ten messages per send. Failures are typed + (`target_busy`, `queued_expired`, `delivery_timeout`); auth and quota failures never retry. + Cross-machine delivery goes over a peer's API server with per-peer keys. + Source: https://hermes-agent.nousresearch.com/docs/user-guide/bot-mode +- **Cron.** A `deliver` target can be another bot's chat (`bot-chat`), which injects the output as + an incoming message. Cron sessions cannot create cron jobs (loop guard). + Source: https://hermes-agent.nousresearch.com/docs/user-guide/features/cron +- **A2A server and client.** Serves an agent card, JSON-RPC task methods, signed push + notifications; `A2A_MAX_PINGPONG_TURNS` defaults to five; audit log on disk. + Source: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/a2a + +### 1.5 OpenClaw (formerly Clawdbot / Moltbot) + +Several agents in one gateway, each with its own state directory, workspace and sandbox. +`sessions_send` is opt-in with an allowlist. The message lands in the target session as a user +message tagged `provenance.kind = "inter_session"`, added after a security advisory where routed +prompts were indistinguishable from real user input. After the target's turn, a bounded ping-pong +loop alternates the two sessions up to `maxPingPongTurns` (default five), then an announce step +lets the target post to its bound channel. Known gaps: the pairwise cap does not catch three-agent +cycles, and announce echoes caused two to three times turn amplification. +Sources: https://docs.openclaw.ai/concepts/multi-agent, https://docs.openclaw.ai/concepts/session-tool + +### 1.6 Buzz (Block, Jack Dorsey, 2026-07-21) + +Apache-2.0 team workspace in the Slack shape (channels, threads, DMs, git, workflows) where +agents are first-class members. Transport is Nostr, not Bitchat: every action is a signed Nostr +event over WebSocket to one self-hosted relay. Identity is a secp256k1 keypair per human and per +agent; agents are addressed by mention or public key. An owner key signs a scoped attestation +authorizing an agent key ("NIP-OA"). The wake model is the interesting part: `buzz-acp` +subscribes to mentions per channel, keeps one prompt in flight per channel, and drives a pool of +stateless agent subprocesses (goose, Codex, Claude Code) over Zed's Agent Client Protocol on +stdio. Hermes ships a Buzz adapter that polls every four seconds. Single relay, no federation. +Sources: https://github.com/block/buzz/blob/main/ARCHITECTURE.md, +https://github.com/block/buzz/blob/main/crates/buzz-acp/README.md, +https://techcrunch.com/2026/07/21/jack-dorsey-is-taking-on-slack-with-buzz-a-group-chat-platform-for-teams-and-their-ai-agents/ + +Buzz is structurally what ChannelGate already is on Slack: a chat surface where mentioning the +bot in a channel starts a turn, with one prompt in flight per conversation. The difference is +that Buzz gives every agent its own signed identity, so two agents can mention each other. + +### 1.7 Standards + +- **A2A** (Linux Foundation, v1.0.0 on 2026-03-12): signed agent card at + `/.well-known/agent-card.json`, Task / Message / Part / Artifact model, JSON-RPC, gRPC and REST + bindings, request-response plus SSE streaming plus webhook push. No mailbox; long work is a + Task the caller polls or is pushed. Authorization is out of scope (IETF drafts propose + transaction tokens). Source: https://a2a-protocol.org/latest/specification/ +- **IBM ACP** merged into A2A in August 2025. **MCP** stays agent-to-tool. ANP and NLIP exist + with little production adoption. + +### 1.8 Best practices that recur across every working system + +1. **A mailbox with provenance**, never an anonymous prompt: the receiver knows the message came + from an agent, which one, and on whose behalf (OpenClaw `inter_session`, Hermes attribution + prefix, Claude's "from another Claude session" framing). +2. **An agent message carries no authority.** It cannot approve permissions, change config, or + escalate; it runs under the receiver's own policy (Claude cross-session, Codex inheriting the + parent sandbox, CrewAI's lack of attenuation cited as a defect). +3. **Bounded ping-pong.** Every system caps rounds (five in OpenClaw and Hermes A2A, three in + Hermes rooms) and forbids recursion (cron cannot create cron). +4. **Opt-in allowlists and per-peer credentials**, with an audit log. +5. **One prompt in flight per conversation** and queueing while busy (Buzz, Claude cloud). +6. **Compressed handoffs.** Send a brief with objective, output format and boundaries; return a + summary, not a transcript. +7. **Humans can see and interject.** Every exchange lives somewhere a person can read and reply. +8. The MAST study (NeurIPS 2025) found 41 to 87 percent failure rates across seven frameworks, + with inter-agent misalignment the second-largest class; structured messages and independent + verification are the fixes. Source: https://arxiv.org/abs/2503.13657 + +## 2. What ChannelGate already has + +- Every conversation is already an always-on agent: its session resumes on demand, its memory + persists, and the daemon outlives every turn. The missing piece is an address and a delivery + path between two conversations. +- `POST /api/runs` can start a turn as a real thread in a named channel and fire a webhook on + completion, but it is admin-key only and unreachable from a container (the egress proxy refuses + loopback), and its runs act as the `api` principal. +- `run_agent_in_background` re-injects a report into the SAME thread when the work finishes. + That re-injection is exactly the "reply arrives later as a new turn" mechanism a peer + message needs, applied to a different origin thread. +- The Slack pipeline drops every message the bot itself posts and every other bot's messages + unless the app is in `trustedApps` (`isIgnorable` in `src/slack/message-normalize.js`). So two + channel agents cannot accidentally talk through Slack today; that is the loop guard that keeps + the current product safe, and any new path must keep an equivalent. +- Durable approvals, the `api` principal (`src/config/api-principal.js`), the redactor, the + usage ledger and the liveness registry are all reusable as-is. +- The skills catalog already knows peer gateways (`src/gateway/skills/peer-sync.js`: MCP over + HTTP with a scoped access token), which is the natural seed for a cross-gateway hop later. + +## 3. Proposal: channel-to-channel agent mail + +The one-line design: **an agent message is a durable, attributed, bounded turn in the other +channel's own Slack thread, delivered by the daemon, with the answer re-injected into the +asking thread.** Nothing crosses the container boundary except text and, optionally, a +daemon-copied attachment. Every hop is visible to the humans of both channels. + +### 3.1 Addressing, discovery and the access rule + +**The access rule (decided 2026-11-06).** An agent acts on a person's behalf, so it may only +message a channel that person can reach, and the answer may only land where everyone who will +read it could have read the target themselves. Concretely, a send is admitted only when ALL of: + +1. the requesting person passes `isAuthorized()` on the target channel (admin or approved, the + target's access policy, a guest grant), AND +2. the requesting person is a live member of the target conversation (admin status does not skip + this; a person who cannot open the target thread may not have an agent write there), AND +3. every member of the SOURCE conversation is also a member of the target, because the reply is + re-injected into the source thread and read by all of them (a public channel can therefore + never message a private one), AND +4. the target channel's manager has switched "accepts agent messages" on. + +These are exactly the semantics the specialist agent directory shipped on 2026-10-21 +(`search_agents`, `src/gateway/agent-directory.js`, verifier in `src/slack/agent-directory.js`): +requester authorization, live chat membership, the source-audience rule, DMs excluded, re-verified +live rather than from cache. `send_agent_message` reuses that verifier as its gate. The checks run +at every hop: when the tool is called, again immediately before the target turn spawns (membership +can change while a message waits in the queue), and again before every reply is re-injected. + +- Discovery is `search_agents`; there is no separate peer list. Managers describe or hide their + agent with `set_channel_agent_profile`; the new `acceptsMessages` flag sits beside `hidden`. + The access rule makes a per-channel sender allowlist redundant, so there is none. +- `send_agent_message({ to, message, reply_wanted, attachment? })` addresses a channel by the id + or name a `search_agents` result returned. +- The person's access decides REACHABILITY, never AUTHORITY: the delivered turn runs as the fixed + `agent:` principal with the requester as attribution only (section 3.2), so a + prompt-injected sender can never borrow the requester's personal tokens or admin bypass in the + target. This is the split the run API already enforces for its `author` field. +- Unattended origins: a schedule's send uses the schedule's creator, re-checked live at fire time + (as admin status already is); a background agent's send uses its author; an API run cannot + send in phase one because its `author` is untrusted attribution. + +### 3.2 Delivery + +1. The tool writes a row to a new `agent_messages` table (migration 43: id, from_slug, + from_thread_key, requested_by user id, to_slug, to_thread_key, hop, round, status, + created/answered timestamps, cost) and returns `{ status: "queued", id }`. The sender ends its + turn; the gateway-usage guide tells it to, exactly like a background job. +2. The daemon posts the message into the target channel as a new thread through `postNotice`: + "📨 Message from #marketing's agent, requested by @Tiberiu: …" with a footer button that + links back to the origin thread. +3. The daemon runs a turn in that thread with `runMessage`, authored by a new fixed principal + `agent:` (`src/config/api-principal.js` gains a sibling): no personal Composio + token, no personal secrets, never an admin author, never `--dangerously-skip-permissions`. + The turn runs under the TARGET channel's mode, container, memory, skills and secrets. The + preamble states the provenance and that the message carries no authority. +4. The target's reply is posted in its thread AND re-injected into the origin thread as a new + turn of the origin session (the `background.js` `_finish` path, generalized to a foreign + thread), attributed "📬 Reply from #sales's agent". The origin session continues with its own + context intact. +5. A human in either thread can reply normally; the thread is an ordinary thread with an + ordinary session. If a person in `#sales` answers before the agent, the agent's turn sees the + human message on its next turn like any other. + +### 3.3 Guards (all of them, from day one) + +- **Rounds.** As built: at most 3 messages per (origin thread, target channel) pair per rolling + hour, and 20 per source channel per hour; the tool refuses with a named reason. +- **Depth.** A turn authored by `agent:*` may send one further message (depth 2) and no more, so a + chain cannot fan out; it may never create schedules, background agents that themselves send, + or "forever" approvals. +- **Cycles.** Impossible at depth one; no hop path is needed. +- **One in flight per pair**, queue otherwise, with the existing per-thread run queue. +- **Budget.** A per-exchange token and cost ceiling, drawn from the same usage ledger; the + license admission call runs on the target's turn exactly as for any other message. +- **Approval gate.** As built, and not optional: `send_agent_message` is a durable control-plane + card in the ASKING thread that the requester (or an admin) confirms, in Auto mode too. The + message lands in another channel on that person's behalf, so the exact text gets a human click + — the human factor against an injected "ask #sales for …". A per-target gate on the receiving + side remains a phase-two option. +- **Provenance is structural.** The `agent:` principal is a fixed identity like `api`; the + message text is untrusted data in the prompt, wrapped and labeled, never an instruction the + daemon acts on. The redactor runs on both sides, so a secret cannot leak by being quoted + across channels. +- **Audit.** `events` rows `agent_message_sent` / `_delivered` / `_answered` / `_refused` with + both slugs and the requesting user, and a row in the channel audit of each side. + +### 3.4 What stays out of scope on purpose + +- No shared filesystem, no shared memory. Sales's memory stays in Sales; what Marketing learns + is what Sales's agent chose to say. Phase one is text only; attachments copied by the daemon + into the target's `uploads/` under the existing byte cap are a phase-two item. +- No shared "room" thread in phase one. A three-way room (two agents and humans in one channel) + needs each agent to post under its own identity; Slack allows `username` and icon overrides on + `chat.postMessage` with `chat:write.customize`, but the inbound side would still see one bot + user, so the room has to be daemon-routed anyway. Phase two. +- No external peers in phase one. A2A is the obvious later hop (an agent card per opted-in + channel, per-peer tokens minted like the skills sync tokens) and would let a Hermes or Buzz + agent address a ChannelGate channel. Phase three, and only after the in-gateway version has + run for a while. + +### 3.5 Where it lives + +| Piece | Location | +| --- | --- | +| Envelope, delivery, re-injection, guards | `src/gateway/agent-mail.js` (new) | +| Tool `send_agent_message` (discovery stays `search_agents`) | `src/mcp/tools/agent-mail.js` (new), registered in `gateway-server.js` behind the capability toolset | +| Principal | `src/config/api-principal.js` (add the `agent:` principal) | +| Schema | `src/db/migrations.js` migration 43 (`agent_messages`) | +| Channel setting | `acceptsMessages` in the agent profile (`set_channel_agent_profile`, `src/gateway/agent-directory.js`), the Access tab of `src/slack/channel-settings.js`, `routes/channels.js`, admin UI | +| Access gate | the directory verifier (`src/slack/agent-directory.js`) called at send, delivery and reply time | +| Guide | `src/gateway/gateway-usage/references/agent-mail.md` (when to use it, the queued-then-end-turn rule) | +| Tests | `test/agent-mail.test.js`: requester authorization, live membership, source-audience rule, re-check at delivery and reply, round and depth caps, cycle refusal, principal has no personal scope, re-injection targets the origin thread, refusal messages | + +### 3.6 A zero-code experiment (not the product) + +For a demo today: add the Composio Slack app to `trustedApps`, and from `#marketing` have the +agent post "@Xavier …" into `#sales` through `composio-agent`. The mention starts a turn in +`#sales`. The reply does not come back, there is no round cap, and if both channels post +through Composio the two agents can loop until a human stops them. It proves the shape of the +interaction, and it shows why the daemon-owned path above is the version to build. + +### 3.7 Suggested phasing + +1. **Phase 1 (one release):** opt-in, one-way ask with reply re-injection, all guards, audit, + channel settings, tests, `FEATURES.md` and `TEST-PLAN.md` entries with a Claude and a Codex + acceptance case (two test channels, one sends, the reply lands in both threads, the third + round is refused). +2. **Phase 2:** multi-round conversations with a visible transcript on both sides, optional + approval gate, cost reporting per exchange in the usage dashboard. +3. **Phase 3:** A2A agent cards for opted-in channels and cross-gateway peers, reusing the skills + peer-token model. diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index db7e6307..5aa758c7 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -1,7 +1,7 @@ # Compatibility matrix -Next release: **0.5.1**, in development on `beta`. Stable promotion awaits full testing and -explicit owner approval of the exact candidate. +Next release: **0.6.5**, prepared from `beta`. Stable promotion awaits full testing and +explicit organization-admin approval of the exact candidate. The component matrix below describes this candidate. ## What the GitHub checks prove @@ -27,8 +27,8 @@ definitions are in `TEST-PLAN.md` and `docs/RELEASE-ACCEPTANCE.md`. | Component | Supported | Release gate | | --- | --- | --- | | Operating system | Linux (systemd) with rootless Podman; Ubuntu 24.04 tested | CI on Ubuntu | -| Node.js | 22.13 minimum; 24 LTS. SQLite FTS5 (channel-memory search index) is present from the later 22.x builds and 24; on a build without it the daemon boots and memory search uses a plain scan | Full matrix CI | -| Claude Code | Pinned nightly target `2.1.306` (official installer) | Real CLI surface + stub transport | +| Node.js | 24.0.0 minimum; current Node 24 LTS recommended for host and container. The exact minimum and latest 24.x run in CI. The optional SQLite FTS5 index still degrades to a plain scan if absent | Full matrix CI | +| Claude Code | Pinned nightly target `2.1.318` (official installer) | Real CLI surface + stub transport | | Codex CLI | Pinned nightly target `0.162.1`. The container login relay relies on three behaviors verified on 0.156.1 (2026-09-26 spike, `TEST-PLAN.md` P4): a turn runs on an `auth.json` with an access token and an empty refresh token; a valid access token is not refreshed because of `last_refresh` age; `login status` and a turn accept a JWT whose signature segment is a placeholder. `codex exec` needs a closed stdin. Re-run the spike on a Codex bump | Real CLI surface + stub transport; the relay spike before promotion | | SQLite | Built-in `node:sqlite` | Migrations + backup/restore quick-check | | VS Code (editor attach) | Any client. Servers for `1.139.0` and `1.138.0` are shared in the channel image (image spec 1.5.0); another version downloads its own into the channel volume as before | Live image test (`npm run test:live-container`) + a real Remote-SSH / attach connection before promotion | diff --git a/docs/ENGINE-CAPABILITIES.md b/docs/ENGINE-CAPABILITIES.md index efaee2d4..847a6c73 100644 --- a/docs/ENGINE-CAPABILITIES.md +++ b/docs/ENGINE-CAPABILITIES.md @@ -21,7 +21,7 @@ The Qwen column covers every harness generated from the Anthropic-compatible **p `src/engines/qwen.js` — today `qwen` (QwenCloud) and `qwen-eu` (Alibaba Cloud Model Studio, EU region). They share this column because they share the CLI and every guarantee in it; what differs per entry is the endpoint, the credential, the shipped and discovered model catalog, and the -gateway default model. +gateway default model and effort. Each is **opt-in**: a missing `engineEnabled` entry means OFF, it is terminal in the failover graph in both directions, and its provider credential (its own `…ApiKey` / `…BaseUrl` pair) is gateway diff --git a/docs/HTTP-RUN-API.md b/docs/HTTP-RUN-API.md index 6344e0cb..453e07c4 100644 --- a/docs/HTTP-RUN-API.md +++ b/docs/HTTP-RUN-API.md @@ -1,8 +1,33 @@ -# HTTP run API conversations +# HTTP run API -Authenticate `POST /api/runs` with `X-API-Key` or `Authorization: Bearer` using the run API key -generated in Settings. An authenticated admin session also works. The Admin UI's HTTP run API -page documents message/file inputs, engine overrides, polling, stopping and completion webhooks. +## Authentication: channel tokens + +Authenticate `POST /api/runs` with `X-API-Key: ` or `Authorization: Bearer `. +The credential to use is a **channel token**: inside a conversation, anyone who may talk to the +agent asks it to `create_api_token` (and `list_api_tokens`, `revoke_api_token`; admins also see +and revoke every token on the Admin UI → API page). A channel token: + +- belongs to **one conversation** — a run it starts is posted there as a thread; `channel` may be + omitted or must name that conversation, otherwise the request answers 403; +- belongs to **one person** — the run acts as its creator with their *current* role: an admin's + token has what an admin's Slack message has (Admin-mode bypass in an Admin channel, admin-only + tools), a member's token is a member. Their channel authorization is re-checked at every run + (403 when they lost access), and their personal connections, secrets and skills apply exactly as + in Slack. The body's `author` field is ignored; attribution is the creator; +- is shown **once**, stored only as a hash, and stops working the moment it is revoked (401); +- can read the status of, list, and stop only the runs of its own conversation. + +### Deprecated: the organization-wide key + +The single run API key under Settings → HTTP run API still works for existing automations. It is +deprecated: it acts as a fixed admin principal in *any* conversation with no person behind it, +and every use logs a deprecation warning (one per hour). Replace it with channel tokens and clear +it. An authenticated admin session also works from the browser. + +The Admin UI's HTTP run API page documents message/file inputs, engine overrides, polling, +stopping and completion webhooks. + +## Conversations To keep separate events from an external conversation in the same Slack thread: diff --git a/docs/LICENSE-KEYS.md b/docs/LICENSE-KEYS.md index 998fc054..d910dc2f 100644 --- a/docs/LICENSE-KEYS.md +++ b/docs/LICENSE-KEYS.md @@ -8,32 +8,42 @@ are fixed for its agreed term. ## Tiers -| Tier | How you get it | Registered channels | Registered users | Active conversations per UTC month | AI messages per conversation per month | +| Tier | How you get it | Registered channels | Registered users | Active chat threads per channel per UTC month | AI runs per channel per UTC month | | --- | --- | --- | --- | --- | --- | -| **No key** | install and run | 20 | 20 | 1 | 500 | -| **Registered (free key)** | create an account with your email on the ChannelGate platform | 20 | 20 | up to the registered channels | 500 | +| **No key** | install and run | 10 (one active channel per month) | 10 | 100 in the active channel | 500 | +| **Registered (free key)** | create an account with your email on the ChannelGate platform | 10 | 10 | 100 in each channel | 500 | | **Enterprise key** | Enterprise License with Makeitfuture | unlimited by default | unlimited by default | unlimited by default | unlimited by default | ## Definitions -- A **conversation** is a distinct channel, group, or direct-message thread on a supported chat - platform, as identified by the Software. Without a key, the first conversation that receives an - AI message in a calendar month is the one the deployment serves for that month. -- **Registered channels** are the persistent conversation records across all connected platforms, - including DMs, groups and the synthetic API conversation. **Registered users** are all stored - human account records, including unapproved users; the synthetic API principal is excluded. - These inventory counts do not reset each month. Non-Enterprise deployments cannot create a 21st - record of either kind. Existing records remain editable and usable after an upgrade introduces - the cap or an Enterprise downgrade; no records are deleted, and growth is refused until below - the applicable cap. Other authorization and monthly message limits still apply. -- Registered inventory caps are hard ceilings in this version: legacy free payloads that omit - them or specify `null` cannot raise them above 20. Signed smaller caps are honored; Enterprise - defaults to unlimited and can carry agreed limits. -- An **AI message** is one engine run started for a conversation. Every origin counts — user - turns, follow-ups, scheduled runs, and background jobs — except the deployment's own - memory-review runs. Months are calendar months in UTC. -- At the monthly limit the assistant answers with a short notice instead of starting a run; it - warns once at 80 %. +- A **channel** is a persistent channel, group or direct-message record on a supported chat + platform. A **conversation** for the 100 allowance is an active chat thread within that channel, + not another channel and not an individual AI message. Each channel has its own UTC-month + allowance. Repeated messages and automation in the same admitted thread reuse its slot. +- **Registered channels** include DMs, groups and the synthetic API conversation. **Registered + users** are all stored human account records, including unapproved users; the synthetic API + principal is excluded. Inventory counts do not reset monthly. Non-Enterprise deployments + cannot create an 11th record of either kind. Existing records remain editable and usable after + an upgrade or downgrade; growth is refused until below the cap, and other limits still apply. +- Free installations allow 100 active threads **per channel per UTC month**, 10 channels and 10 + users. Legacy or newly signed Free payloads cannot raise these compiled ceilings by omitting + fields, supplying `null`, or granting larger values. Signed smaller caps are honored. + Enterprise defaults to unlimited and may carry agreed limits. +- Without a key, the first active channel admitted in a UTC month remains the one served that + month; its thread allowance is 100. A registered Free key allows activity in its registered + channels, each with its own 100-thread allowance. +- Threads already admitted before a stricter limit or Enterprise downgrade remain usable for + that UTC month. New threads are refused once their channel's allowance is reached. The set + resets at the next UTC month boundary; admitted threads still obey the AI-run allowance. +- An **AI run** is one engine run started for a channel. User turns, follow-ups, schedules and + background jobs share the existing 500-run allowance per channel per UTC month. Memory-review + runs are excluded. The 100-thread allowance does not grant 500 runs to each thread. +- Thread tracking begins with post-upgrade activity. The upgrade does not fabricate past thread + history: an old thread counts when it is next used, and existing channel AI-run counts remain. +- At a monthly limit the assistant answers with a short notice instead of starting a run. The + existing AI-run allowance warns once at 80 %. + +Standalone automation fires without an existing chat thread use a stable logical conversation identity for that fire. Once a delivery thread is known, later runs in that thread share its slot. Background agents and question continuations in an existing conversation keep the parent conversation identity even when their engine sessions are isolated. ## Whose key @@ -143,21 +153,26 @@ unreachable in both directions: it can neither raise nor lower a tier. ## Counting, exactly -- The counters are keyed on `(UTC month, conversation id)` in the local `license_usage` table - (schema migration 12). Nothing else reads or writes them. -- A run is counted **at spawn**, so a refused turn never counts and a burst of concurrent turns - cannot slip past a cap that is already reached. -- The conversation id is the qualified id from `src/platforms/ids.js`, so the same channel name on - two different chat platforms is two different conversations. +- The existing `(UTC month, channel id)` `license_usage` ledger counts AI runs and legacy + active-channel admission. Its `conversations` signed limit retains this channel-ID meaning for + compatibility; it is not the 100-thread product allowance. The Free default of 100 there is + redundant with the 10-channel inventory cap; stricter signed active-channel limits still apply. +- `conversationsPerChannelPerMonth` is the signed thread allowance: 100 for Free and no-key, + unlimited (`null`) by default for Enterprise. The separate monthly thread ledger is keyed by + channel and the gateway's resolved thread identity. Platform-qualified channel IDs prevent + collisions between identically named channels on different platforms. +- Admission and counting happen before engine spawn, atomically, so a refused turn does not + count and concurrent first messages cannot exceed a cap. A run without a thread identity uses + the gateway's stable unthreaded identity in that channel; it cannot create unlimited slots. - Memory-review runs are excluded, by origin (`memory_review`) and by toolset (`CG_TOOLSET=memory-review`). -- The 80% warning is posted once per conversation per month, prefixed to the answer the user is +- The 80% AI-run warning is posted once per channel per month, prefixed to the answer the user is already receiving — it costs no extra message and cannot be missed. ## Seeing the state - Admin UI: **Settings → License** — tier, key last four, when it was last verified, when the next - check is due, the state banner, and this month's per-conversation usage with the limit line. + check is due, the state banner, and this month's per-channel AI-run and active-thread usage with their limit lines. - Chat: the `get_license_status` gateway tool. - API: `GET /api/license` (admin session), and `POST /api/license/verify` to check now. @@ -182,3 +197,35 @@ Only an effective Enterprise license permits changing the policy. A saved restri enforced after downgrade or expiry; restoring Enterprise is required to change or clear it. The default on existing channels is unrestricted. This governs gateway-managed harness launches, not arbitrary commands a Worker/Admin author is otherwise allowed to execute inside a container. + +## Cost-tracking history + +Non-Enterprise installations expose one month (30 days) of cost history in audit and dashboard reports. Enterprise history is unlimited. Older accounting remains stored for upgrades; license usage counters and conversation history are independent. Structured costs older than the reporting window are also hidden in audit-event, API-job and schedule-telemetry responses, while their operational records and result text remain available. + +## Enterprise organization controls + +The following capabilities require an effective Enterprise license (including use of previously +saved configuration after downgrade or expiry): + +- Skill preferred-model switching: opt a conversation into honoring a skill's engine/model preference. +- Exact company email-domain auto-approval for new users and policy application to existing users. +- A dedicated Codex login per conversation, separate from the gateway login. +- The external skill-catalog MCP endpoint and its catalog/user access tokens. +- Organization environment variables shared across conversations. +- Google Drive workspace synchronization, both scheduled and on demand. +- Channel skill templates and their inherited skill grants. +- Per-channel monthly USD spending limits. + +Other tiers retain ordinary skills, channel/personal environment variables, the shared gateway +Codex login, manual user approval and standard chat connectors. Stored Enterprise configuration +is preserved when an entitlement expires; it does not activate Enterprise behavior. A channel +configured with a dedicated Codex login is refused rather than silently switched to another account. + +A spending limit is an admission control against the channel's recorded spend in the current UTC +calendar month. It is not a provider billing limit: an in-flight run can take recorded spend above +the cap, concurrent runs can overshoot, and provider-side costs are not reserved in advance. +`monthlySpendingLimitUSD: null` means no cap; a configured cap is a positive finite USD amount. +A current-month usage row with unknown cost refuses new runs until pricing is repaired or an +admin clears the cap. A saved cap also refuses new runs after downgrade; an admin can clear it +without Enterprise, so the safety control never silently disappears. +See the regression checks under **Enterprise organization controls** in `TEST-PLAN.md`. diff --git a/docs/LICENSING-FAQ.md b/docs/LICENSING-FAQ.md index 7c0e734f..6a284ea4 100644 --- a/docs/LICENSING-FAQ.md +++ b/docs/LICENSING-FAQ.md @@ -173,3 +173,21 @@ so an integration being useful to one team does not imply it is included in the **Can a no-key install include the EE directory?** Yes. Its license expressly permits unchanged bundled enforcement code in every otherwise permitted install and distribution. Enterprise feature use still requires the corresponding entitlement; see [the EE terms](../src/ee/LICENSE-EE.md). + + +**Which organization controls require Enterprise?** Skill preferred-model switching, exact company +email-domain auto-approval, dedicated channel Codex logins, external skill-catalog MCP and access +tokens, organization environment variables, Google Drive sync, channel skill templates and monthly +per-channel USD spending limits. Manual user approval, ordinary skills, channel/personal secrets, +the shared Codex login and standard chat connectors remain available in other tiers. Existing +Enterprise configuration survives expiry but does not continue activating these capabilities. +See [license-key behavior](LICENSE-KEYS.md#enterprise-organization-controls). + +**Does a spending limit cap my provider invoice?** No. It blocks new gateway runs when the +channel's recorded spend reaches its configured limit for the current UTC calendar month. +In-flight and concurrent runs may exceed the limit; it does not reserve provider spend or control +usage outside this gateway. + +### Mandatory organization policies + +Enterprise is required to add mandatory organization skill grants or add/edit organization-wide agent instructions. Previously saved policies continue applying after a downgrade so a license change never silently removes protections. Administrators can explicitly remove grants or clear instructions on any tier. Existing Lean-mode exclusions still apply; these policies do not override Lean mode. Personal and channel skill grants, ordinary catalog content maintenance, channel instructions and the built-in safety baseline remain available on all tiers. diff --git a/docs/LICENSING-SUMMARY.md b/docs/LICENSING-SUMMARY.md index 10b1b5c0..a918e843 100644 --- a/docs/LICENSING-SUMMARY.md +++ b/docs/LICENSING-SUMMARY.md @@ -15,6 +15,10 @@ and any separate agreement with the Licensor. This summary does not replace them it does not unlock optional Enterprise features or permit disabling enforcement. - **Composio SDK mode is Enterprise-only and Beta.** Standard Composio MCP mode remains available in every tier. Google Chat and Microsoft Teams are Beta connectors available in every tier. +- **Enterprise organization controls:** skill preferred-model switching, exact company email-domain + auto-approval, dedicated per-channel Codex logins, external skill-catalog MCP/tokens, organization + environment variables, Google Drive sync, channel skill templates and monthly per-channel USD + spending limits. [Behavior and limits](LICENSE-KEYS.md#enterprise-organization-controls). - Filesystem/process isolation, authorization, secret handling, audit records and backup/restore retain the same protections in every tier. diff --git a/docs/MAINTAINER-RELEASE.md b/docs/MAINTAINER-RELEASE.md index 56c407c1..64682a30 100644 --- a/docs/MAINTAINER-RELEASE.md +++ b/docs/MAINTAINER-RELEASE.md @@ -5,6 +5,12 @@ Use [RELEASE-CHECKLIST.md](RELEASE-CHECKLIST.md) for mandatory gates and and evidence; maintainers own production access, private acceptance fixtures and release signing. Keep private account aliases and test-channel identifiers in deployment-owned records. +## Release version selection + +Keep releases on **0.6.x**. Advance to **0.7** only when the project owner explicitly requests +that minor-version change; feature scope alone does not authorize it. The current candidate is +**0.6.5**. + ## Candidate evidence The release workflow accepts only the package version's exact tag on main. It checks the lockfile, diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 0bd3aba8..58e372bf 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -1,5 +1,52 @@ # Operations runbook +## Dashboard users, passkeys and recovery + +Use the existing **Users** list. Give the intended person the **Admin** role, open their details, +and choose **Invite to dashboard**. Share the generated single-use setup link privately; it expires +in 15 minutes and grants only passkey registration. Once registered, the user signs in with +**Sign in with a passkey**, without entering a username or password. Settings → Admin access +manages the signed-in user's keys. Add a spare authenticator before losing the primary device. + +Existing passkeys are preserved. On first use after migration, select the owning admin from the +existing directory and request a private verification code. Enter it in the same browser within +five minutes. This binds the existing credential to that user's stable ID without replacing it. +The code is delivered only through the user's native private chat, never a public channel or API +response. Google Chat users need a previously learned native user resource for private delivery; +missing delivery configuration fails closed. Unlinked users must finish verification before using +ordinary dashboard APIs. Names and matching email labels never silently assign credentials. + +Before the first passkey, **First-time setup** accepts the existing operator password. If known +admins exist, their private-code identity proof is required before enrollment. A fresh install +without known admins can configure its first connection temporarily; once admins exist, identity +linking is required. First enrollment permanently disables shared-password login and revokes its +sessions. Setup/recovery cannot restore password fallback. + +Configure the final HTTPS **Public URL before enrollment**. Keys belong to that origin/hostname; +changing domains does not transfer them. Only explicit loopback origins permit local development +without HTTPS. Public URL changes require recent passkey verification after enrollment. + +Removing a person's admin role revokes dashboard authority immediately, including existing sessions, +setup links and pending authenticator results. Restoring the role requires a new sign-in; old +sessions remain invalid. Deleting their user record also denies access. Role removal is allowed +for the last enrolled admin, so keep another enrolled admin or host recovery available. Revoke +sessions in the user's details when needed. Your final own key cannot be deleted normally. + +For lost keys, run **on the gateway host as its daemon OS user**, from the checkout: + +```sh +npm run admin:recover -- --user +``` + +The target must currently be an admin. Recovery revokes all its keys/sessions and prints one +registration-only URL valid for 15 minutes. Open privately at the configured HTTPS URL and enroll +a replacement. Treat that URL as a credential. The legacy `--username` option is reserved for +unlinked migration/bootstrap accounts; it cannot substitute for a known linked user's role. + +Sessions clear on restart and expire after 30 minutes idle or 12 hours total. Sensitive mutations, +new authenticators and secret reveal require passkey verification within five minutes. The run +API bearer key remains scoped automation access and cannot enroll keys or log into the dashboard. + ## Backup and restore Stop the daemon before restoring. Interactive restores require typing `RESTORE`; unattended @@ -96,26 +143,29 @@ allowed past the DNS-rebinding guard (extra names go in `CG_ALLOWED_HOSTS`). **Client addresses behind that hop.** Every request then arrives on a loopback socket, so `req.ip` — and Express's idea of the client — is `127.0.0.1` for every caller on earth. Two things count failures per address: the admin login backoff and the approval-link token backoff. Keyed on -the socket they would share ONE bucket, and a handful of bad tokens or passwords from anywhere -would lock out every legitimate admin and every valid approval link. So both read the real client -through one helper (`clientKey` in `src/web/security.js`): `CF-Connecting-IP` first, then the first -hop of `X-Forwarded-For`, and **only when the socket itself is loopback** — from a non-loopback -socket those headers are whatever the client typed and are ignored. A value that is not a valid IP -address is ignored too. +the socket they share ONE bucket, and a handful of bad tokens or passwords from anywhere delays +every legitimate admin and every valid approval link. So **declare the proxy** in *Settings → +Connection → Behind a reverse proxy* (`trustProxy` in settings.json, or `CG_TRUST_PROXY` in +`.env`; the setting wins): `cloudflare` makes both limiters key on `CF-Connecting-IP`, which +cloudflared overwrites on every request; `xff` keys on the **last** hop of `X-Forwarded-For`, the +one your nginx/caddy appended (never the first, which the client typed). Without it, all remote +logins share one backoff bucket. A loopback socket on its own is deliberately NOT taken as proof +of a proxy — any process on the host or any tunnel opens one — so an undeclared header is ignored, +and a value that is not a valid IP address is ignored too. Independently of the key, more than 50 +failed attempts in ten minutes across all clients puts every login on the maximum delay until the +window drains, which bounds guessing even where a header is misdeclared. Notes for operators: - Express's app-wide `trust proxy` is deliberately NOT enabled: it would also re-point `req.secure` / `req.protocol` / `req.hostname` at client-supplied headers (the Host/Origin guard and the session cookie's `Secure` flag read those headers themselves, deliberately), and it knows - nothing about `CF-Connecting-IP`. -- If your proxy runs on ANOTHER host, the socket is not loopback and the headers are ignored by - default. Set `CG_TRUST_PROXY=1` in `.env` to honour `X-Forwarded-For` there — only do this when a - proxy you control rewrites that header, or clients can pick their own limiter bucket. -- Make the proxy set `X-Forwarded-For` rather than append to a client-supplied one. Where it - appends (Cloudflare does), `CF-Connecting-IP` is the unforgeable value and is preferred here; a - caller who spoofs the first XFF hop can only shed its own backoff, never push someone else into - one. + nothing about `CF-Connecting-IP`. The `Secure` cookie decision still reads `x-forwarded-proto` + on its own; that only ever makes the cookie stricter. +- Only declare a proxy you control and that sets the header on EVERY request. If clients can reach + the daemon's port directly (a LAN bind) while `xff` is declared, a caller could append its own + hop; keep the bind on loopback or firewall the port, as the rest of this guide assumes. +- The legacy `CG_TRUST_PROXY=1` still works and means `xff`. ## The ChannelGate rename migration @@ -358,7 +408,7 @@ image until idle. A manual `git pull` still needs a matching image rebuild. | Claude token for container runs | the output of `claude setup-token` on the gateway host — write-only | | Full-access channels see the gateway home | off by default; on = current admin authors in Admin channels mount the gateway user's home; members keep a separate project container (see below) | | Legacy open network (no egress proxy) | off by default (`containerEgressMode = "proxy"`); on (`"bridge"`) = the pre-proxy behavior: the open bridge network and raw secret values (see **Network** below) | -| Withhold readable variables | `containerEgressSecretsStrict`: ON for a new install, OFF for an install that existed before this default (stored once at boot; your choice is never changed); on = a secret with no egress rule is not given to proxy-mode containers at all, off = it is injected raw and listed as unprotected | +| Withhold automatically readable variables | `containerEgressSecretsStrict`: ON for a new install, OFF for an install that existed before this default (stored once at boot; your choice is never changed); on = a secret with no egress rule is withheld unless its stored choice is explicitly `readable`, off = it is injected raw and listed as unprotected | Values that would reach the container CLI's argv are validated at the boundary: a flag, a space or a shell metacharacter in the image/memory/cpu fields is rejected with an error, not silently cleaned. @@ -396,7 +446,10 @@ channel's policy to every request: - **Raw sockets** (`ssh`, `psql`) get a plain CONNECT tunnel only while the network is on and only to `github.com:22` plus the hosts an admin lists in the channel's `egressRawHosts` (admin API: `PUT /api/channels//meta` with `{"egressRawHosts":["db.example.com"]}`), on ports 22, 5432 - and 6543. A client has to be pointed at the proxy for that (a `ProxyCommand`); nothing else + and 6543. An agent may request one exact public host using `allow_channel_ssh_host`: its durable + card can be approved by any currently authorized channel member with write access, even in Auto + mode. Approval appends rather than replaces hosts; Network must be on, and private-address + checks still apply on every connection. Admins retain the direct API path. A client has to be pointed at the proxy for that (a `ProxyCommand`); nothing else leaves the container. - A channel that genuinely needs arbitrary raw sockets can be given the open bridge BESIDE the proxy: `{"rawNetwork": true}` on the same admin API. The proxy env stays set, so proxy-aware @@ -429,11 +482,17 @@ as the useless string it is. When declaring **Used on hosts**, avoid multi-tenan deployment too, and a container could have the real value swapped into a request to a site it controls. Rotation is live (the proxy resolves the current value per request); removing a secret revokes its placeholder, and re-adding -mints a new one. A secret with no rule is withheld entirely while *Withhold readable variables* -is on (the default on a new install; the run's credential note names it as withheld), or injected +mints a new one. An automatically readable secret with no rule is withheld entirely while +*Withhold automatically readable variables* is on (the default on a new install; the run's +credential note names it as withheld), or injected raw and listed as **unprotected** in the admin UI and the run's own credential note while it is off. Either way `list_secrets` ends with a **Finding** naming each such secret; declare its *Used on -hosts* (or `hosts` with `set_secret`) to give containers a placeholder instead. +hosts* (or `hosts` with `set_secret`) to give containers a placeholder instead. If a CLI or another +program needs the raw value, explicitly choose `set_secret_mode` with `mode: "readable"` in the +owning conversation and scope, then approve its durable card. That stored choice permits raw +injection even with strict mode on; no other secret receives an exception. `list_secrets` reports +these separately as **Explicit readable**. Only a new process receives the change. Returning to +`auto` or `hidden` restores normal classification and strict withholding when no rule applies. **A secret with no rule is decided by its kind.** One that looks like a web API token is **hidden**: containers get a placeholder the proxy swaps in a credential-like header or query parameter (Authorization, x-api-key, `api_key=`, `token=`…), but only on servers an admin approved for it. The first time a program sends it to a new server, the proxy @@ -449,7 +508,7 @@ a mail or database password, a connection string, a signing key — or that is c `…_ID`, `…_REPO`, `…_REGION`) stays **readable** (raw, listed unprotected). `set_secret_mode` overrides the decision either way. `SUPABASE_DB_PASSWORD` and other raw-protocol passwords can never be swapped: they stay raw -(withheld under strict). +(withheld under strict unless explicitly made readable). **What the proxy refuses on the way.** Inside a tunnel the request target must be a path (an absolute-form `GET https://other/…` line is `400 absolute-form-in-tunnel`); a `Host` header naming @@ -557,6 +616,35 @@ and isolated database extractor without granting tunnel privileges to ordinary c See [CHANNEL-VPN.md](CHANNEL-VPN.md) for protected profiles, channel Secrets, systemd persistence, database-only routing and live acceptance requirements. +**Make.com CLI in every runtime image (spec 1.6.3).** `make-cli` is the official +[`@makehq/cli`](https://github.com/integromat/make-cli), pinned in +`containers/versions.json` and installed root-owned by the default image build. New deployments +receive it with the image; existing deployments rebuild with `npm run build:image` and let the +normal image-ID lifecycle recreate their containers. This is the Make automation CLI, separate +from the GNU build tool. + +Set `MAKE_API_KEY` in the channel's Secrets and `MAKE_ZONE` to its actual API zone (for example +`eu2.make.com`). Set the zone's mode to **Readable** when strict secret handling requires an +explicit configuration grant. Use `make-cli --version`, `make-cli --help` and a read-only command +such as `make-cli scenarios list --team-id=` to verify setup. The upstream CLI +reads these names, not the older `MAKE_API` or `MAKE_API_TOKEN` names: no automatic credential +alias or host-account fallback is supplied. The API key's placeholder is swapped only on Make +hosts; the routing zone must remain a real hostname. Existing `MAKE_API_TOKEN` API integrations +retain their egress rule. + +A deliberate interactive `make-cli login` saves credentials only under that channel's HOME at +`~/.config/make-cli/config.json`; a host's saved login is never imported. Prefer channel Secrets +for headless automation so credentials retain their egress protection. Saved login state, like +other per-channel HOME data, survives a recreate. + +**Node runtime prerequisite.** The daemon, installer, updater and documentation build require +Node 24.0.0 or newer; use the current Node 24 LTS patch on the host. This baseline satisfies the +Composio SDK and agent-browser requirements. The default container base is +`node:24-bookworm-slim`, and its resolved Node version is verified during image acceptance. +Upgrade the host Node installation before updating an older deployment, retain its previous +runtime for rollback, and restart the daemon safely. Rebuilding the container alone does not +upgrade the host Node installation. + **Installing tools in a channel.** An agent can install whatever it needs, and it stays installed — everything below writes inside the per-channel HOME volume: @@ -1007,3 +1095,63 @@ editor. Disabling the policy or removing a domain does not revoke existing acces Preview existing Slack users before applying a batch (up to 500 known users). Apply fetches fresh profiles and rechecks the current policy and manual opt-outs. Lookup failures grant no access and are counted in the result. Non-Slack users are checked on their next authenticated message. + +## Rootless Podman system-service upgrades + +The system-service installer writes only its managed +`~/.config/containers/containers.conf.d/99-channelgate-system-service.conf` for the dedicated +service account. It sets `cgroup_manager="cgroupfs"`: container processes stay beneath the +system unit's `Delegate=yes` hierarchy, so `podman exec` does not cross into a different +systemd ownership subtree. Some rootless cgroupfs versions do not create a per-container +subgroup at all; successful execution does not imply resource-limit enforcement. +A systemd-manager container created beneath `user@UID.service` can start +successfully yet reject every exec from a daemon in `/system.slice/channelgate.service`. +The installer checks the effective manager and refuses a conflicting later configuration; +it preserves the operator's main `containers.conf` and other snippets. + +The unit bind-mounts service-owned mode-0700 `service-tmp` and `service-var-tmp` directories +under the service HOME onto `/tmp` and `/var/tmp`. This keeps temporary files private while +preserving their backing paths when Podman's rootless pause namespace outlives a daemon +restart. `PrivateTmp` is disabled because its backing directories are deleted on stop. +Existing temporary contents are retained; there is no automatic deletion or pruning. + +Changing those settings does not repair an existing stale pause namespace or migrate +containers already created with another cgroup manager. For an **existing installation**, an +operator first arranges an idle maintenance window (no turns, background jobs, SSH/editor +sessions or other users of that account's Podman store), then: + +1. Stop the gateway service. Confirm the service username, HOME and UID from the unit rather + than assuming the fresh-install defaults. +2. As that service account, from a traversable working directory and with its own `HOME` and + `XDG_RUNTIME_DIR=/run/user/`, inspect its named containers. Remove only stopped gateway + containers that must be recreated after the manager change, by exact name with `podman rm` + (without volume-removal options). Retain every channel HOME/tmp/var-tmp named volume. +3. Run `podman system migrate` as that account while its containers are stopped. This resets + the account's shared pause namespace and affects its entire container store. If it reports + a stale conmon exit, inspect the named container's state; do not proceed as if migration + succeeded. Reconcile only the identified stopped containers, then rerun migration until it + exits successfully. Do not put this operation in an unconditional `ExecStartPre`. +4. Run `sudo bash scripts/install-systemd.sh --no-start` from the intended installed revision + to install the managed configuration and persistent temp mounts without starting the daemon. + Remove any operator-added unconditional `podman system migrate` startup drop-in only after + the idle migration succeeded; keep unrelated overrides. Start the service and verify + `podman exec` through an actual gateway turn, then restart + while idle and repeat. Require normal file access inside `/home/agent`, accurate resource-limit + status (including the unenforced warning where applicable), and no `cgroup.procs` permission + or deleted-namespace/pasta error. Source/image changes + and per-channel state follow the normal deployment rules. + +The resource-limit probe uses `--network none --pull never`: it needs neither pasta nor +registry access and never downloads a missing image. Missing image, mount namespace, timeout +and other launch failures are reported as **unverified limits**, separately from a positively +identified cgroup delegation failure. Probe diagnostics omit raw runtime output. + +A zero exit from `podman run --memory ...` or matching `podman inspect` settings is not proof +of enforcement. In particular, rootless `cgroupfs` without an explicitly prepared cgroup parent +can accept CPU/memory/pids flags yet leave processes in the service's unrestricted cgroup. The +probe now requires actual kernel values for all three limits, inside the test container. An +unenforced or unreadable configuration disables advertised per-container limits and produces +an explicit warning; the gateway follows its existing run-without-limits fallback policy. This +installer fixes container execution but does not add a delegated-parent cgroup supervisor. +Operators requiring enforced per-container limits must use a correctly delegated configuration +and verify the probe succeeds; configured values alone are insufficient. diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md index eb905ff7..2ca2a0c3 100644 --- a/docs/PLATFORMS.md +++ b/docs/PLATFORMS.md @@ -442,6 +442,10 @@ Native settings dialogs are available; broadcast mentions remain unavailable. - Agents use `share_file` to deliver **Open file** in the current Teams conversation. No Composio connection or public link is needed; the gateway Public URL is required for its authenticated browser downloads. Slack-only tools are not exposed to Teams or Google Chat. +- **Code channels are Slack-only.** `open_code_channel`, `update_code_channel` and + `archive_code_channel` register only where the `codeChannels` capability is declared (Slack). The + Slack app needs `features.code_channels.enabled` and the `code_channels:manage` scope from + `slack-app-manifest.json`; Slack decides eligibility per workspace and account at creation time. - `/settings` → MCPs → **My connections** saves or disconnects personal Composio and Toolbox tokens inside the Teams window. Stored values are never prefilled; inputs are masked. - Native approvals stay on their source-thread card without supplemental decision-link DMs. @@ -574,3 +578,11 @@ status. Validation challenges have their own `validated` event and do not prove delivery. Diagnostics exclude tokens, request headers, bodies and unverified identifiers. If no receipt exists, correlate provider delivery with ingress/security logs; an empty queue cannot distinguish a blocked POST from an absent provider notification. + +The subscription's JSON `resource` selector uses literal Teams identifiers, for example +`/teams//channels/19:example@thread.tacv2/messages`. Percent encoding belongs to the +message-fetch HTTP path, not to this selector. `subscriptionResource` records the registered +form; ordinary renewal cannot change a provider subscription's resource. A deployment with an +older encoded registration and validation-only audit evidence needs a scoped replacement, not +another renewal or restart. Preserve the conversation's activation cutoffs and actor checks; +confirm authenticated change-notification delivery before retiring the prior registration. diff --git a/docs/RELEASE-ACCEPTANCE.md b/docs/RELEASE-ACCEPTANCE.md index 2374ab51..6ea1ca84 100644 --- a/docs/RELEASE-ACCEPTANCE.md +++ b/docs/RELEASE-ACCEPTANCE.md @@ -1,5 +1,10 @@ # Release acceptance packet +Current candidate: **0.6.5**, prepared 2026-11-06. See [the candidate record](releases/0.6.5.md). +Current-candidate execution is pending; the historical evidence and deferrals below apply only +to their named revisions. They are not passes or waivers for 0.6.5. Human-only checks are +assigned in the private QA handoff; this public packet contains no private actor identities. + 0.5.1 live evidence (2026-09-21, owner-approved release without the full campaign): on the exact candidate tree, a live Claude turn used the Read tool and returned the probe file's bytes; a live Qwen turn with no model configured answered from QwenCloud with real tokens and no dollar cost. diff --git a/docs/RELEASE-CHECKLIST.md b/docs/RELEASE-CHECKLIST.md index fe776101..4a1ec7cc 100644 --- a/docs/RELEASE-CHECKLIST.md +++ b/docs/RELEASE-CHECKLIST.md @@ -1,3 +1,8 @@ +> **0.6.5 candidate preparation (2026-11-06):** see [the candidate record](releases/0.6.5.md). +> Stable promotion is pending exact-candidate automated and live acceptance evidence and +> organization-admin approval. Historical approvals and deferrals below do not authorize this +> candidate. No release tag or publication is authorized by preparation alone. + > 0.6.3 API conversation candidate (2026-10-11): focused backport of `ac523624`, > migration 32 schema prerequisite to preserve upgrade ordering, and the compatible dependency > lock refresh from `6d72d299`. No unrelated beta runtime changes are included. Package version diff --git a/docs/SKILLS.md b/docs/SKILLS.md index 47c5f08a..d0bc27db 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -1,4 +1,9 @@ -# Skills — the local catalog, channel profiles, templates, authoring and sources +# Skills — the gateway catalog, channel profiles, templates, authoring and sources + +**Enterprise:** channel skill templates, the external catalog MCP endpoint/access tokens and +honoring skill preferred-model switches require an effective Enterprise license. Ordinary local +skills and authoring remain available in other tiers. Saved Enterprise configuration is retained +after expiry without activating those behaviors; see [license keys](LICENSE-KEYS.md#enterprise-organization-controls). ChannelGate keeps its own **skill catalog** in the gateway database and materializes real skill folders into every conversation that is granted them. Skills work with no external token and no @@ -7,6 +12,12 @@ network call at run time; Claude and Codex both discover them natively. This is the Core (self-hosted) plane of the skills platform. The Enterprise cloud plane (a hosted library, fleet sync, a universal Skills MCP) builds on the same catalog and is a separate offering. +## Where agents must save skills + +Create and grant skills through the ChannelGate catalog using `create_skill` and the gateway grant tools. Do not install active skills by writing directly into `.claude/skills`, `.agents/skills`, or engine HOME skill folders: gateway reconciliation replaces managed copies and archives unmanaged workspace entries. An on-disk export is only an export, not a durable active channel skill. This rule overrides a generic skill creator's local-folder default. + +Repository-synced skills can be used read-only without a write token. To create or modify a skill in an external source repository, first verify that an available token or authenticated connection has write access to that exact repository and tracked branch. A token name or successful read does not prove write access. Only then edit, commit, push and sync the source; verify the effective revision. Without write access, treat the source as read-only and create any additions as a catalog companion skill. Do not add an external repository as the writable home for a new skill unless write access is established. Never request or paste a token in chat; use the gateway secret/connection flow. Do not bypass this by editing materialized files or pinning a catalog override. + ## What a skill is A folder with a `SKILL.md` (YAML frontmatter + Markdown body) and any number of sibling files @@ -26,9 +37,23 @@ category: Sales version: 1.2.0 tags: [crm] requires: [makeitfuture-organization] # catalog skills granted together with this one +model: sonnet # optional: the model this skill works best with --- ``` +**`model:` (optional) — model-specific skills.** Name the model the skill works best with +(`sonnet`, `claude-opus-5-5`, `gpt-6.1-sol`, …); the engine is derived from it. Nothing else is +declared — the gateway decides how to honor the preference when the skill is actually *used*, not +when it is read: every materialized copy carries one line asking the model to call the gateway tool +`use_skill` as it starts following the skill. In a conversation whose **Allow skill engine and +model swapping** switch is on (off by default; `set_channel_skill_swap`, the admin Runtime pane or +the Slack Settings ticks), and when nobody pinned the thread with `/model`, a model of the current +engine resumes the same session on the preferred model and the thread stays there; a model of the +other engine is delegated to a background agent on that engine. A pin, the switch being off or an +unavailable engine leave the skill on the conversation's model. `create_skill` and `update_skill` +take an optional `model` argument that writes (or, with `""`, clears) this line; the library's +skill detail shows the preferred model. + Every skill has one **owner**: | owner | how it gets in | how it changes | @@ -36,7 +61,9 @@ Every skill has one **owner**: | bundled | shipped in this checkout (`src/gateway/skills/bundled/`), imported at boot | a new gateway release | | folder | the host's `~/.claude/skills`, `~/.agents/skills` or `GATEWAY_SKILL_SOURCES`, imported at boot and re-read by content | edit the folder; the next boot / re-import picks it up | | git | a GitHub source (below) | the source's next sync; a skill of the gateway's own publish repository also takes `update_skill` (pushed back) | -| local | authored from chat (`create_skill`) or the admin UI | `update_skill` — see *Who may change what* below | +| gateway-owned (`local` internally) | authored from chat (`create_skill`) or the admin UI | `update_skill` — see *Who may change what* below | + +**Gateway-owned** means managed directly in this gateway's catalog, independently of whether the skill is personal, channel-scoped or organization-wide. It does not mean an unmanaged file installed in an engine skill folder. Publishing to the configured Git source adopts the skill into that source; it remains editable through `update_skill` when that source is the gateway's writable publishing repository. Internal API/database ownership values remain `local` for compatibility. A slug is owned by exactly one owner. A sync or an import that finds a slug another owner holds reports a **conflict** and writes nothing. A skill whose source this gateway cannot write to @@ -93,7 +120,7 @@ in every other conversation) and back on with `add_channel_skills`. `set_skill_s `POST /api/skills/catalog/:slug/scope` move a skill either way: **promoting** a customer skill to the library moves its files in the repository and leaves the channel with an explicit grant, so nothing changes there; demoting keeps it for that channel only. Only skills the publish -repository owns (or unpublished local ones) move — a skill synced from another source stays with +repository owns (or unpublished gateway-owned ones) move — a skill synced from another source stays with its source. Names must be unique across the whole repository (the slug comes from the skill's name), so customer skills are prefixed with the customer. @@ -229,10 +256,15 @@ it can never reach the organization tier. above). The bundled `skill-authoring` skill teaches the contract and how to write a description that triggers. - `update_skill` — a new revision. Pass only the files that change; the rest are carried over - (`remove` drops files). A local skill is revised in place; a skill synced from the configured + (`remove` drops files). A gateway-owned skill is revised in place; a skill synced from the configured publish repository is revised and pushed back to its own folder (if the push fails the new - revision is pinned, so the next sync cannot revert it). A skill pinned to an older revision is - re-pinned to the new one. A non-admin's edit of an organization skill becomes a `change` + revision gets a publication recovery pin, so the next sync cannot revert it). A successful + retry through `publish_skill` or a later successful update clears that recovery pin and follows + the published revision again. Manual pins and approved source overrides retain their reason + when advanced to a new edit and are never automatically cleared. Existing pins from before + provenance tracking remain manual/legacy; their origin cannot be inferred safely. `get_skill_info` + reports current and effective revisions plus an explicit pin reason or "none". + A non-admin's edit of an organization skill becomes a `change` proposal automatically. Any other source is read-only: `update_skill` explains how to extend it with a companion skill. - `delete_skill` — a catalog tombstone: an admin, or the author of a personal skill. Anyone else @@ -242,7 +274,7 @@ it can never reach the organization tier. organization skill gets granted everywhere), a catalog delete (`kind: delete`), or feedback. - Admins review with `list_skill_proposals` / `decide_skill_proposal` in chat, or in the admin UI (Skills → Review). An approved change becomes one revision; when the skill is source-owned the - revision is **pinned** as a local override, so the source keeps flowing into later revisions + revision is **pinned** as a gateway override, so the source keeps flowing into later revisions and the pin holds until an admin unpins. An approved promotion moves a channel skill into the library (the channel keeps it as an explicit grant), makes a personal skill an organization skill, or adds an organization skill to the organization tier; an approved `delete` tombstones @@ -272,7 +304,7 @@ publishes it). Any approved member can also carry organization skills in their o ## Publishing to Git Admin UI → Skills → Sync settings → *Publishing to Git*: a GitHub repository, folder and its own -write-only token. Publishing follows `main`. Every new revision of a local skill — created, +write-only token. Publishing follows `main`. Every new revision of a gateway-owned skill — created, updated, an approved change, an approved promotion — is pushed with the GitHub Contents API, one commit per file under `//` (`.` as the folder = the repository root) — or under `channels///` for a channel-scoped skill, with a README naming the channel — and @@ -305,7 +337,16 @@ tokens*: random, shown once, stored hashed, revocable, with scopes: | sync | `library_export`, `library_export_skill` (what a peer gateway needs) | Claude Code: `claude mcp add --transport http channelgate-skills /mcp/skills --header -"Authorization: Bearer "`. Personal skills never appear over MCP. +"Authorization: Bearer "`. + +### Two kinds of token + +| kind | minted by | reaches | +| ------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| catalog | an admin, in the admin UI (scopes above) | the LIBRARY: organization and channel skills (also non-discoverable ones for `sync`). Never a personal skill — not listed, read, exported or edited. `manage` writes library skills. For other systems and peer gateways. | +| user | a person, from chat (`create_my_skill_token`) | that person, resolved LIVE on every call: organization skills (granted or discoverable), the skills of every channel whose access policy admits them right now (own grants, template, section), and their own personal skills. `read`, `propose`, `manage` with the chat rules (personal: author; channel: members; organization: admins); never `sync`. Dies when the owner is no longer approved; up to 5 active per person; the owner lists/revokes theirs, admins see and revoke all in the admin UI. | + +The bearer is verified before the request body is parsed, and invalid tokens back off per client. ## Peer gateways @@ -329,7 +370,7 @@ with the gateway's environment, uses the Skills Manager tokens the old integrati 3. with `SM_SUPABASE_URL` + `SM_SUPABASE_SERVICE_KEY` (Skills Manager's service role): team favorites become templates (Development; Sales & Marketing → sales and marketing; Management; Admin), admin exclusions become tombstones, and each token user's personal skills become - personal local skills. + personal gateway-owned skills. The script is idempotent and prints what it would do with `--dry-run`. Stub folders the old integration left in channel folders are pruned automatically on each channel's next message. diff --git a/docs/SSH-ACCESS.md b/docs/SSH-ACCESS.md index 386c3097..08cabbe8 100644 --- a/docs/SSH-ACCESS.md +++ b/docs/SSH-ACCESS.md @@ -1,8 +1,9 @@ # SSH access to channel containers Developers can open a real SSH session — terminal, VS Code Remote-SSH, sftp, port forwards — -inside a channel's own container, the same box the assistant works in, without any account on -the gateway host and without any container ever listening on a port. One key per person, +inside the channel container selected for their current role, matching their own ordinary chat +turns, without any account on the gateway host and without any container ever listening on a +port. One key per person, registered once from chat; users enable their own access per channel without manager or admin approval; the container stays up for as long as a session is open. @@ -105,7 +106,17 @@ machine settings — merge-only, and never over a value you set yourself. Inside, you are user `agent` in the channel's work folder (an interactive login starts there; image spec 1.5.1), with the same environment an engine turn gets and the channel's persistent -`/home/agent` (installed tools, `gh`/`vercel`/`supabase` logins, Claude and Codex history). +`/home/agent` for their runtime scope (installed tools, `gh`/`vercel`/`supabase` logins, Claude and +Codex history). Current organization admins use the admin container and legacy HOME (`cg-…`); +members and guests use the separate project container and HOME (`cgp2-…`). The work folder is +shared, but native engine history and CLI logins are separate. "Show SSH access" names the +requester's scope. SSH rechecks roles while connected; a scope change disconnects the session +before preparing more credentials, and reconnecting selects the current scope. The existing +Admin-mode + `containerFullAccessHome` refusal still applies to everyone. + +An upgrade correcting SSH scope selection can return an admin to their existing `cg-…` history. +Sessions created in the project's `cgp2-…` HOME stay there; no history is deleted or automatically +copied across the boundary. Ordinary members continue to use project history. Codex uses the channel's selected login (a proxy placeholder in proxy mode, a host file mount in legacy bridge mode), and `codex` in a session gets what a chat turn's Codex gets: the gateway tools, your own Composio accounts as `composio-user`, the channel's as @@ -146,10 +157,11 @@ session gets: account" with the operator's organization and email, `/usage` shows the plan's windows, and the default model is the plan's. It is the operator's login (the same one every turn uses): your session's usage counts against it. Claude reads the login from an access-only file the daemon - writes and refreshes; it holds no refresh token and is removed when the channel's last session + writes and refreshes; it holds no refresh token and is removed when the container's last session ends. Behind the egress proxy its access token is the channel's relay placeholder (the plan, expiry and rate tier beside it are the real login's — they are facts, not secrets), so the file - is useless outside the container. `claude -r ` resumes a thread's own session. + is useless outside the container. `claude -r ` resumes a native session stored in + the same runtime scope; admin, project and `/sudo` host histories are separate. Codex over SSH uses the same selected login as a channel turn. "show SSH access" in the channel prints what a session gets. If a part could not be prepared — no Claude login on the @@ -204,7 +216,7 @@ egress proxy (image spec 1.6.0). An SSH session is inside that container, so the client downloads the server and copies it in over the session. - **The editor token file.** `/vscode/claude-token` (read by the `claude` wrapper outside an SSH session, e.g. a `podman exec` shell or an operator's `npm run vscode` window) holds - the same relay placeholder, and is removed when the channel's last session ends as well as when + the same relay placeholder, and is removed when the container's last session ends as well as when the `npm run vscode` launcher exits. An open `npm run vscode` window counts as live work for the channel's own and organization secrets and the Claude login, never for anyone's personal ones. @@ -271,7 +283,7 @@ ended); `show_channel_ssh` lists the live ones. | `/etc/ssh/sshd_config.d/channelgate.conf` | the Match block for the login account, plus an `AllowUsers`/`AllowGroups` line when the host restricts logins | | `///ssh/` | `sshd_config` (its one `SetEnv` carries the container env + the proxy set), `authorized_keys`, `host_key` for the container (identical path inside) | | `///ssh/users//` | one developer's `env` (proxy set first, then secrets — placeholders behind the proxy), `session.md`, `settings.json`, `mcp.json`, Codex's `codex-args.sh` + `codex-secrets.json` (the capability only) | -| `///vscode/claude-token` | the Claude login the wrapper reads outside an SSH session (the relay placeholder behind the proxy); removed with the channel's last session | +| `///vscode/claude-token` | the Claude login the wrapper reads outside an SSH session (the relay placeholder behind the proxy); removed with the container's last session | | `gateway.db` → `ssh_keys`, `ssh_sessions`, `events` | keys, sessions, audit | - *`channelgate-ssh@…: Permission denied (publickey)`* straight away, with nothing in the daemon @@ -321,4 +333,11 @@ ended); `show_channel_ssh` lists the live ones. network-off …`* — outbound SSH needs *Allow network* on; any other host must be a declared raw host. `kex_exchange_identification: Connection closed` right after it is ssh's own echo of that. - *Connection drops on daemon restart* — expected; reconnect. +- *History differs from another person's Slack session* — compare runtime scopes first. Admin + and project users share the work folder, but not HOME or native history. Your own ordinary + chat turns and SSH use the same scope at your current role. +- *Host-key warning after a scope change* — each scope keeps its own key at + `/ssh/host_key`. Ordinary container rebuilds preserve that file. Ask an operator + to verify the selected scope's fingerprint before updating the matching known-host entry; + a changed key by itself is not proof of a harmless rebuild. - `journalctl -u channelgate | grep '\[ssh\]'` shows binds, sessions and relay refresh failures. diff --git a/docs/releases/0.6.5.md b/docs/releases/0.6.5.md new file mode 100644 index 00000000..f05f8abe --- /dev/null +++ b/docs/releases/0.6.5.md @@ -0,0 +1,49 @@ +# 0.6.5 stable promotion candidate + +Prepared 2026-11-06. Status: **not approved for stable promotion**. + +The promotion PR head identifies the exact candidate. Freeze that SHA in the PR and the private +acceptance manifest after the Sunday cron fix lands. Later beta commits are excluded unless +reviewed, retested and explicitly included in a replacement candidate. This record is a preparation +contract, not a claim that the checks below have run. + +## Scope + +Promote the integrated beta history since stable, including channel-to-channel agent messages, +Enterprise organization controls and channel spending limits, skill model selection and publication +recovery, email sign-in, SSH destination approval and status bars, Claude usage accounting, +queued Slack forks, runtime recovery fixes and the Sunday `7` cron alias correction. + +The runtime requires **Node.js 24.0.0 or newer**. Upgrade the host before updating an older +installation. Runtime image spec is **1.6.3**; rebuild the image for the pinned toolchain and Make +CLI, then verify the actual image/container generation before live acceptance. The full supported +component matrix is in [COMPATIBILITY.md](../COMPATIBILITY.md). + +Unmerged Teams settings batching and Files dialog work is excluded. Microsoft Teams and Google +Chat live campaign holds remain in force. Existing integrated transport fixes remain part of the +source candidate; a hold does not mark their acceptance as passed. + +## Evidence required before promotion + +- [ ] Exact candidate SHA and source range recorded in the promotion PR. +- [ ] Full coverage suite and static checks pass on the candidate. +- [ ] Tracked and candidate-history secret scans pass. +- [ ] Dedicated security coverage and high/critical production dependency audit pass. +- [ ] Every candidate commit passes the stable-range DCO check. +- [ ] Candidate CI and documentation checks pass. +- [ ] Active executable private QA cases are inventoried with their Claude/Codex matrix, + current-candidate results, precise evidence, unresolved defects and approved holds. +- [ ] Human-only cases have an explicit owner and reproducible handoff; assignment is not a pass. +- [ ] Upgrade, backup/restore and rollback prerequisites are checked for the target deployment. +- [ ] An organization admin approves the exact tested candidate after reviewing the evidence + and explicitly deciding any remaining live-case deferrals. + +No tag, main merge, customer update or published release is performed by this preparation. +After approval, retain the previous release/runtime snapshot through the rollback window, +promote only the approved SHA, and keep beta aligned with stable history. For a tagged release, +create the exact package-version tag on main and run the release-evidence workflow. Independently +verify its image inventory, model records, archive, checksums and attestations before distribution. +Unsigned local build metadata is not signed release provenance. + +Historical release evidence is retained in [RELEASE-ACCEPTANCE.md](../RELEASE-ACCEPTANCE.md) +and [RELEASE-CHECKLIST.md](../RELEASE-CHECKLIST.md); it is not relabeled for this candidate. diff --git a/documentation/README.md b/documentation/README.md index 10519d4a..58af5b7e 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -7,7 +7,7 @@ website home, blog, and other routes keep their own build and assets. ## Build and preview locally -Run these commands from this directory on Node.js 22.13 or newer: +Run these commands from this directory on Node.js 24.0.0 or newer: ```sh npm ci @@ -41,7 +41,7 @@ The website keeps its existing Vercel configuration, including its API rewrites. The handbook groups practical guides by chat work, files/reports, engines, memory, skills, connections, security, developer tools, platforms, automation, administration, and operations. -The expanded catalog is **152 pages**: **107 feature guides**, **13 configuration guides**, +The expanded catalog is **153 pages**: **107 feature guides**, **14 configuration guides**, **13 control references**, four directories/maps, and 15 retained overview, walkthrough, technical-reference and resource pages. `src/data/handbook.mjs` supplies feature/configuration navigation; `control-reference.mjs` supplies control navigation. `functionality-coverage.mjs` diff --git a/documentation/astro.config.mjs b/documentation/astro.config.mjs index fb9725f6..c824a1e6 100644 --- a/documentation/astro.config.mjs +++ b/documentation/astro.config.mjs @@ -21,6 +21,7 @@ export default defineConfig({ { label: 'Welcome', slug: '' }, { label: 'Your first conversation', slug: 'getting-started' }, { label: 'Installation', slug: 'installation' }, + { label: 'VPS sizing and hosting', slug: 'configuration/vps-sizing' }, { label: 'All features', slug: 'features' }, { label: 'Functionality map', slug: 'functionality' }, { label: 'Control reference', slug: 'controls' }, diff --git a/documentation/package-lock.json b/documentation/package-lock.json index 4d4e49d4..c56eb7ed 100644 --- a/documentation/package-lock.json +++ b/documentation/package-lock.json @@ -8,38 +8,38 @@ "name": "channelgate-documentation", "version": "0.1.0", "dependencies": { - "@astrojs/starlight": "0.42.5", - "astro": "7.3.5" + "@astrojs/starlight": "0.42.9", + "astro": "7.4.7" }, "engines": { - "node": ">=22.12.0" + "node": ">=24.0.0" } }, "node_modules/@astrojs/compiler-binding": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.1.tgz", - "integrity": "sha512-oczyolLUl9mNrtuBuNjuvn5thpHrFLXAHAWGTRg0JDHQ8cGVsVeFekOu2uSoQGgainJIi+A3/9m0DyYwz3Nlnw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.2.tgz", + "integrity": "sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@astrojs/compiler-binding-android-arm64": "0.5.1", - "@astrojs/compiler-binding-darwin-arm64": "0.5.1", - "@astrojs/compiler-binding-darwin-x64": "0.5.1", - "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.1", - "@astrojs/compiler-binding-linux-arm64-musl": "0.5.1", - "@astrojs/compiler-binding-linux-x64-gnu": "0.5.1", - "@astrojs/compiler-binding-linux-x64-musl": "0.5.1", - "@astrojs/compiler-binding-wasm32-wasi": "0.5.1", - "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.1", - "@astrojs/compiler-binding-win32-x64-msvc": "0.5.1" + "@astrojs/compiler-binding-android-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-x64": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.2", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.2", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.2", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.2", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.2" } }, "node_modules/@astrojs/compiler-binding-android-arm64": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.1.tgz", - "integrity": "sha512-nMb2nMIJtcWLNH+fV75Enmi+jp4tIOYOfsv4XtpgdOWkqc9P4uSSdK5u1xb340AzrXo5V18SxLl/nc3vcqtamQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.2.tgz", + "integrity": "sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==", "cpu": [ "arm64" ], @@ -53,9 +53,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-arm64": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.1.tgz", - "integrity": "sha512-5k4+nZ/WRt+dcVow4z7rBuI7iv9A58h/uQ91YJe1PrTivbcCkq3u2Ynx/AZJwII4CsZawBCeGpojkFjutZBhBQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.2.tgz", + "integrity": "sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==", "cpu": [ "arm64" ], @@ -69,9 +69,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-x64": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.1.tgz", - "integrity": "sha512-0Buw+YST9Ff2EqQyhRMQujuFq/dijLw3zXWhsvI0dJ2pGrT38iVoxTEca2eBX/PfhlwkVWSRJEKO5gMqKHyeHg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.2.tgz", + "integrity": "sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==", "cpu": [ "x64" ], @@ -85,12 +85,15 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.1.tgz", - "integrity": "sha512-x5LRpyCoMKuu3INr18cn42YhwSOaRpp9b5Ny3eJJ/sWVeejC16Zeshu1JlvlSmEZACLeWxU5JM0RX6T6nI+YyA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.2.tgz", + "integrity": "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -101,12 +104,15 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.1.tgz", - "integrity": "sha512-mL7tQEuDk3rGfaKmrO63nw6WBy1TPVioQGmVyjcAr+C+a/k3SVMYBytNSMqH9mZPT/Swj30JaZ2N842Z1NnOEQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.2.tgz", + "integrity": "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -117,12 +123,15 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.1.tgz", - "integrity": "sha512-uLGc9jtxwiDe4cQyhNPQim4ECiB/YzbWlXssfLAv8r374kArfTwBHjwR2+j+XVyF71yCx5mF9YNRclRqdNBgeg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.2.tgz", + "integrity": "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -133,12 +142,15 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-musl": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.1.tgz", - "integrity": "sha512-G0cywinaQUc8v1MrytvYKUX7svTDMmYtOCZfxjwKPbJi59zIqM+Ll0rWgO0GQWLZH3XoPYo19ZwlP3Xt9ep9YA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.2.tgz", + "integrity": "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -149,9 +161,9 @@ } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.1.tgz", - "integrity": "sha512-yMhLL8asmLydoAdnEZxAw6eLfNDs3YJl6jqn0pjPt6zrb2U5qIDLnii4/twThpIFODKkQ6YDpBRTOz/vSfZCxw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.2.tgz", + "integrity": "sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==", "cpu": [ "wasm32" ], @@ -160,16 +172,16 @@ "dependencies": { "@emnapi/core": "1.11.3", "@emnapi/runtime": "1.11.3", - "@napi-rs/wasm-runtime": "^1.2.4" + "@napi-rs/wasm-runtime": "^1.2.5" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.1.tgz", - "integrity": "sha512-aClIG6poFt3Wf/bZykrEAX2nAnKvBHW6RXiKUa69QjBtWH0/FOlOAjnFcxRNlhej2AMWNYFx0o+LGjwMcH8P7Q==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.2.tgz", + "integrity": "sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==", "cpu": [ "arm64" ], @@ -183,9 +195,9 @@ } }, "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.1.tgz", - "integrity": "sha512-/E6uBCagY70YpWqssBC+/FuniNSloeQPRgj7l3LDWiqjxIyTLnIgX1sTt18FU7jrigetpSnC9cHAM6b6IxSgPw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.2.tgz", + "integrity": "sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==", "cpu": [ "x64" ], @@ -199,57 +211,247 @@ } }, "node_modules/@astrojs/compiler-rs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.1.tgz", - "integrity": "sha512-1k1acnX7DepIiea9U9/Y+d1hdAFfylu1ZQQkkYgRSwBOBAsXInolyZuh/2tax+83vu/A+hOYP6P3+265RvMTvw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.2.tgz", + "integrity": "sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==", "license": "MIT", "dependencies": { - "@astrojs/compiler-binding": "0.5.1" + "@astrojs/compiler-binding": "0.5.2" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@astrojs/internal-helpers": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.11.0.tgz", - "integrity": "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.1.tgz", + "integrity": "sha512-LgGLGzoC0wCT3mjDYrxz+hct/tws0enpWsLJW3bswHR3PKgusp/H9xRIT8rTdsyvLM0TcRhhvRL30J2oaZNcqA==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", - "js-yaml": "^4.3.0", - "picomatch": "^4.0.4", + "picomatch": "^4.0.7", "retext-smartypants": "^6.2.0", - "shiki": "^4.0.2", - "smol-toml": "^1.6.0", - "unified": "^11.0.5" + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" } }, "node_modules/@astrojs/markdown-satteri": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.2.tgz", - "integrity": "sha512-g4QehXnB/MzxQWnSf17NciFInMqEJy9948p8cc7zGg/r7995m0Ln5PMr3Xlnj24iey1LchaoZ3SzTOa3D4P/3w==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.7.tgz", + "integrity": "sha512-MZvnec/898GGGHQZfkArVbkdZmL1imjIM8RWkmzbAmse737zr8D9inUjFQ5eLBXV3vPobOUs/ZRV/D6Po/bVDg==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/prism": "4.0.2", + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/prism": "4.1.2", "github-slugger": "^2.0.0", - "satteri": "^0.10.3" + "satteri": "^0.11.6" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.11.6.tgz", + "integrity": "sha512-kcr7QOPGrckuyhMZmUKYcyoXgjQN4vJ/6tCWHE2866mztZPNrTuy5itTwSAbIs1tK0XpTW42tBQ+XPNnkg0XIg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-darwin-x64": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.11.6.tgz", + "integrity": "sha512-nwh4Atd/Cxw2SNS6+Gs0Esf/w2i4qyHzwcECaTgoFyxse3m0dpBpu8eqkqqIPfBc1lWhfe4i9LXuk7rG+4pwQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.11.6.tgz", + "integrity": "sha512-A7cHeCZQkvF3oTgQ6/5LvVriauE1dHo3u3gAME6H1X/+KMeum5Wras3Y7rYUcvPym1CxdmzjZh9WlYD62Ul1xg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.11.6.tgz", + "integrity": "sha512-P/YAudp6VmUr63SFaZefwPIefsw92oZ1wQ6vqrtO/GY9qB3OxLqUVjSIcnKAtqMp4kDW8xBEdSwHz4GZU7P2+g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.11.6.tgz", + "integrity": "sha512-nhj90eZFLSA+Icd6n+oYpAi/UXr/mppLGd8jdEOB/larvWAZ/ewFxpT7CLzuNCg7r75+gkdHv9JuRb5eERY1Kg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.11.6.tgz", + "integrity": "sha512-mkWxwHd5Q2dDs+OZK/P1tZFvSPrh2Bb69utWHcBNM0wyJCRWd61T4jU1tF21lGL1C5dy7P+LvaT0y6PQDJj89Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.11.6.tgz", + "integrity": "sha512-GrjNThC6BfA0u8LZwGpOeEFVKoJZsriowLtj/rBYj18fW7/UZouyzn/0Tsxg+tMaxMUgC9HFA078EmfAsdVONg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.11.6.tgz", + "integrity": "sha512-Plesa0QYtJoa3nlObg1Ypj9oD1VOioHMldHUUtG61T/JNU11EcN0pyj0tDsT3DW2pAxKrV0Q8xrfzGtBHuSjfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.11.6.tgz", + "integrity": "sha512-fGs5d/7jBIPkYmXkhm9unoEfDuRZ5r66NLxbr+6c8bm0f7ywgPHgW3HJ9L7fKvyqhw49ePtbT4vkWREju8/U6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/satteri": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.11.6.tgz", + "integrity": "sha512-ddGCTqSfwL+A+odhdjj8E+ZMu1h0QEnG3VTmu7fPJL2+80GdjxDh1AeCw8mn2aa95eQSosG4e2QBYYfCc5nyiA==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-android-arm64": "0.11.6", + "@bruits/satteri-darwin-arm64": "0.11.6", + "@bruits/satteri-darwin-x64": "0.11.6", + "@bruits/satteri-freebsd-x64": "0.11.6", + "@bruits/satteri-linux-arm64-gnu": "0.11.6", + "@bruits/satteri-linux-arm64-musl": "0.11.6", + "@bruits/satteri-linux-x64-gnu": "0.11.6", + "@bruits/satteri-linux-x64-musl": "0.11.6", + "@bruits/satteri-wasm32-wasi": "0.11.6", + "@bruits/satteri-win32-arm64-msvc": "0.11.6", + "@bruits/satteri-win32-x64-msvc": "0.11.6" } }, "node_modules/@astrojs/mdx": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.2.tgz", - "integrity": "sha512-WedIVP2Bu8fXSzxb3ZYZ8xDrWWjf2SRKCneh52Llhoa+O3lcpHPhoZVP3FerlivUaH/0LJz2yVaCNvMdg0u8Bw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.1.3.tgz", + "integrity": "sha512-Fd72Sy0AvFqXSC/RR7icvNi27T5KWNojnvuHgu38lx6RkevRzLWDxzHk3sbtL2z17yvVr7j28KCyHcLr2jQ7Pg==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/markdown-satteri": "0.4.2", - "es-module-lexer": "^2.0.0" + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/markdown-satteri": "0.4.7", + "es-module-lexer": "^3.0.4" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.23.4" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0", @@ -263,15 +465,15 @@ } }, "node_modules/@astrojs/prism": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", - "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.2.tgz", + "integrity": "sha512-jMrUEpFDarDO1KWPvVc9eSa8DSfwBlwEOHwX+8zvZQpiqXRGDxFnOXHsYqV6DP7w+0qIgTr98gS1GdQlm8ypWg==", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.23.4" } }, "node_modules/@astrojs/sitemap": { @@ -285,17 +487,16 @@ } }, "node_modules/@astrojs/starlight": { - "version": "0.42.5", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.5.tgz", - "integrity": "sha512-S6hjUexk9rrKtjJF3WR8T9dM1woNR7X2XU8e3wj4OpRT4X15AXk/mzPjWe/Qd5+P8whHj2+ves+k06qirBzimg==", + "version": "0.42.9", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.9.tgz", + "integrity": "sha512-kMqDdFDWBClBNY9fc3pWAlR+eFh6JhOd7lIr4RczNDXUVk9EzzxVQvYizRuxlx0aVbSk8Gb1rVHiJlxgybwc/g==", "license": "MIT", "dependencies": { - "@astrojs/markdown-satteri": "^0.4.0", - "@astrojs/mdx": "^8.0.0", - "@astrojs/sitemap": "^3.7.3", + "@astrojs/markdown-satteri": "^0.4.3", + "@astrojs/mdx": "^8.0.3", + "@astrojs/sitemap": "^3.7.4", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.5", - "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.44.0", "bcp-47": "^2.1.0", @@ -305,7 +506,6 @@ "hast-util-to-string": "^3.0.1", "hastscript": "^9.0.1", "i18next": "^26.0.7", - "js-yaml": "^4.1.1", "klona": "^2.0.6", "magic-string": "^1.2.3", "mdast-util-directive": "^3.1.0", @@ -317,7 +517,8 @@ "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3" + "vfile": "^6.0.3", + "yaml": "^2.9.1" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0", @@ -330,18 +531,18 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", - "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.2.tgz", + "integrity": "sha512-2JOMRUxJrBSfiVDfeKLv1wBmvSIx1Y6u2YEO8awZ9qSq3u/rNAiUyFdOYi8dHv91kHrmYEQX7pRDw0J4xyj3UA==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", - "package-manager-detector": "^1.6.0" + "package-manager-detector": "^1.9.0" }, "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" + "node": ">=22.23.4" } }, "node_modules/@babel/helper-string-parser": { @@ -390,6 +591,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bruits/satteri-android-arm64": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-android-arm64/-/satteri-android-arm64-0.11.6.tgz", + "integrity": "sha512-9Cb6ZRDn29c0kluGA9urp/So/JnJOJJSf4J40xYLAIAj3GuqTNsCyquS4A1/yLuuHE2ngYJNnCylAQnDM3L9ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@bruits/satteri-darwin-arm64": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", @@ -416,6 +630,19 @@ "darwin" ] }, + "node_modules/@bruits/satteri-freebsd-x64": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@bruits/satteri-freebsd-x64/-/satteri-freebsd-x64-0.11.6.tgz", + "integrity": "sha512-qMkNZRYN3cLshfYawQYjfDS05A208QCOVh1adtEteYH+03MkuOoVVhYkzDRIKhWdfM3gFFEaUjXDOGfVqGjEJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@bruits/satteri-linux-arm64-gnu": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", @@ -623,488 +850,427 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], + "node_modules/@expressive-code/plugin-frames": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@expressive-code/core": "^0.44.2" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@expressive-code/core": "^0.44.2" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "license": "MIT", + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "freebsd" ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "linux" + "darwin" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ - "ia32" + "x64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "linux" + "darwin" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ - "loong64" + "arm" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ - "mips64el" + "arm64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "netbsd" + "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "netbsd" + "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ - "arm64" + "arm" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ - "arm64" + "ppc64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ - "x64" + "riscv64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "sunos" + "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ - "arm64" + "s390x" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@expressive-code/core": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", - "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^4.0.4", - "hast-util-select": "^6.0.2", - "hast-util-to-html": "^9.0.1", - "hast-util-to-text": "^4.0.1", - "hastscript": "^9.0.0", - "postcss": "^8.4.38", - "postcss-nested": "^6.0.1", - "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1" - } - }, - "node_modules/@expressive-code/plugin-frames": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", - "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.44.2" - } - }, - "node_modules/@expressive-code/plugin-shiki": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", - "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.44.2", - "shiki": "^4.0.2" - } - }, - "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", - "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", - "license": "MIT", - "dependencies": { - "@expressive-code/core": "^0.44.2" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" } }, - "node_modules/@img/sharp-darwin-arm64": { + "node_modules/@img/sharp-linuxmusl-arm64": { "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", - "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">=20.9.0" @@ -1113,20 +1279,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, - "node_modules/@img/sharp-darwin-x64": { + "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", - "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": ">=20.9.0" @@ -1135,20 +1301,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, - "node_modules/@img/sharp-freebsd-wasm32": { + "node_modules/@img/sharp-wasm32": { "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", - "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "os": [ - "freebsd" - ], "dependencies": { - "@img/sharp-wasm32": "0.35.5" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -1157,444 +1320,500 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", - "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ - "arm64" + "wasm32" ], - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0", "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", - "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ - "x64" + "arm64" ], - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "win32" ], + "engines": { + "node": ">=20.9.0" + }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", - "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ - "arm" + "ia32" ], - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "linux" + "win32" ], + "engines": { + "node": "^20.9.0" + }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", - "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ - "arm64" + "x64" ], - "license": "LGPL-3.0-or-later", + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ - "linux" + "win32" ], + "engines": { + "node": ">=20.9.0" + }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", - "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.7.tgz", + "integrity": "sha512-zJccwplOAbgfZju95IUHazLnx9RAPtPN2YGJXI6UbrgeaR9QfPjGrQL6ERkSAtnVCA4Q+83ApGyaX5zdpCaKkQ==", + "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", - "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", + "license": "MIT", "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", - "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", "cpu": [ - "s390x" + "arm64" ], - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "darwin" + ] }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", - "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", - "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", "cpu": [ "arm64" ], - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } + ] }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", - "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", "cpu": [ "x64" ], - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" ], - "funding": { - "url": "https://opencollective.com/libvips" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", - "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", "cpu": [ "arm" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", - "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", - "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", "cpu": [ - "ppc64" + "arm64" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", - "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", "cpu": [ - "riscv64" + "x64" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", - "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", "cpu": [ - "s390x" + "x64" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", - "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", "cpu": [ - "x64" + "arm" ], - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", - "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", "cpu": [ "arm64" ], - "license": "Apache-2.0", + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", - "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", "cpu": [ - "x64" + "arm64" ], - "license": "Apache-2.0", + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", - "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.3" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", - "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", "cpu": [ - "wasm32" + "s390x" ], - "license": "Apache-2.0", + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.5" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", - "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", "cpu": [ - "arm64" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "libc": [ + "glibc" + ], + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", - "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", "cpu": [ - "ia32" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "libc": [ + "musl" + ], + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", - "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", "cpu": [ - "x64" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ - "win32" + "openharmony" ], "engines": { - "node": ">=20.9.0" + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.3.0.tgz", + "integrity": "sha512-ntK6OFor3nQtS1TenZP5wX59oPpkKEmour3AipGHB32u+Z8QXxjVdG+7vNSvgerRWIj6vjg4XpKu42slb0vlXQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "2.0.0-beta.2", + "@emnapi/runtime": "2.0.0-beta.2", + "@napi-rs/wasm-runtime": "1.2.6" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "license": "MIT" + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-beta.2.tgz", + "integrity": "sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "2.2.0", + "tslib": "^2.4.0" + } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", - "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-beta.2.tgz", + "integrity": "sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.2.0.tgz", + "integrity": "sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", "license": "MIT", "optional": true, "dependencies": { @@ -1608,304 +1827,488 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", - "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT" - }, - "node_modules/@oxc-project/types": { - "version": "0.152.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", - "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" } }, - "node_modules/@pagefind/darwin-arm64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", - "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@pagefind/darwin-x64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", - "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "darwin" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@pagefind/default-ui": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", - "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, - "node_modules/@pagefind/freebsd-x64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", - "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@pagefind/linux-arm64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", - "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/engine-javascript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@pagefind/linux-x64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", - "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@pagefind/windows-arm64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", - "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/langs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@pagefind/windows-x64": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", - "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/primitive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", - "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", - "cpu": [ - "arm" - ], + "node_modules/@shikijs/themes": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@shikijs/types": "4.5.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", - "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", - "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", - "cpu": [ - "arm64" - ], + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/am-i-vibing": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 8" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", - "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", - "cpu": [ - "x64" - ], + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.7.tgz", + "integrity": "sha512-fH54IOaCkZY1paEjygKn8CPfuJ3w8ns7PourcPML5AouJkQuTBqBQ2RbbIbBhutZbg2qn5se6Yl9DYybzqjC0Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.5.2", + "@astrojs/internal-helpers": "0.13.1", + "@astrojs/markdown-satteri": "0.4.7", + "@astrojs/telemetry": "3.4.2", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.5.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^2.0.1", + "devalue": "^6.0.3", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^3.0.4", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.6", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.3.0", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.4.5", + "magicast": "^0.5.5", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^10.0.0", + "package-manager-detector": "^1.9.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.7", + "rolldown": "^1.2.16", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "tinyps": "^0.5.1", + "ultrahtml": "^1.7.0", + "unifont": "^1.0.3", + "unstorage": "^1.17.6", + "verkit": "^0.5.0", + "vite": "^8.3.5", + "vitefu": "^1.1.3", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.6.7" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.23.4", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/astro-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", - "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", - "cpu": [ - "x64" - ], + "node_modules/astro/node_modules/@oxc-project/types": { + "version": "0.157.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.157.0.tgz", + "integrity": "sha512-95iTox1qdt6WGWce3xwNOVayByOfLaYAq+qGPpub6NWbBh9sv0GG3+zyfh9ZBCO/jVu5A0EWI+qQI7g6fevjxg==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "funding": { + "url": "https://github.com/sponsors/oxc-project" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", - "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "node_modules/astro/node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.3.0.tgz", + "integrity": "sha512-zjq+pUSE36RJG9nJXqrRfMCOxekzVhlaYMtFELHaOcRH4UqTnkWhbTbEixKEawsb0ZTVMRbn8RLYcuU4DYk8qg==", "cpu": [ "arm" ], "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", - "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "node_modules/astro/node_modules/@rolldown/binding-android-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.3.0.tgz", + "integrity": "sha512-HdnCiSmMGgEILGVLU1o4dcOblLGj8sMnqEFrDY/S7KRti1nTQHAqGGs2AniPspbK90AFS3pM23K/R0J3hPQXYQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", - "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "node_modules/astro/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-TkQ2JGC8/9UxoCeH5dSJj1gxuFhzw200QTJJd2YNPRXznSSM4XvpKV2Cs+RM2BcM8sa6xa4rmXNuiJ64+1993g==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", - "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" + "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", - "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "node_modules/astro/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Gr+iZW50RJZo4BPUazaDh/Azx7W0Rl04sy2MkXD+H4RkxzUX5hIrjaVw1BK+XHsccKExQAmCy6DhvRLlUzaYUg==", "cpu": [ - "s390x" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", - "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "node_modules/astro/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-RwAtx8peSqUoBf8pxE0L62xKXBNnA3BtdFV+07t13t0TQoASCtV9tnU67aKKMRZSiD06SU07chB5B8fWFdW3gQ==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", - "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "node_modules/astro/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-ow8fq6kYaIx75D0EVT5GeF4DcawcEYYXJ2CvEaOzyygW1jtVDQU/9o5YYidctHBRDUgPzGaTNB6n/VMTmd1ymw==", "cpu": [ - "x64" + "arm" ], "license": "MIT", "optional": true, @@ -1916,416 +2319,200 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", - "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "node_modules/astro/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-PLOWuiz+rdqSgiLgN7bxvy6DjXLj8gnJxzRS243LNnLi96VeyB7VFsC3eblcKv3Xwd3jQqJfbCWRhLijTIZyvQ==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", - "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "node_modules/astro/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-CQ/k85cc342j5SnZ/gT9p132NXZRGTgLRhfItdYqUg+jpvL5HYFve0GB+zQDCYGwDjtDWIoqKSOitkmZMZc/tw==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", - "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "node_modules/astro/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.3.0.tgz", + "integrity": "sha512-wk+ZG7xDP2px3cQRFoNoNp52/w1O5hTTtciHxuastreMOp8isYUTCpcG/rEYnSQBI+nycN+B9d9pBGua+qvV7w==", "cpu": [ - "x64" + "ppc64" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" + "libc": [ + "glibc" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "license": "MIT" - }, - "node_modules/@shikijs/core": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", - "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", - "license": "MIT", - "dependencies": { - "@shikijs/primitive": "4.5.0", - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5", - "hast-util-to-html": "^9.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", - "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.6" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", - "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/langs": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", - "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/primitive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", - "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/themes": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", - "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/types": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", - "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", - "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/nlcst": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", - "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/node": { - "version": "24.19.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", - "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "node_modules/astro/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.3.0.tgz", + "integrity": "sha512-sEgyZt72Rj7V5q6OZ7A4U2I5v4EMGEv3Fp5QmoncmKbUMTrGmpyH7MwENfERGbfUYjxdBmktw4JJLys5I1aQNQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/sax": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", - "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "node_modules/astro/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-PvL0BoBUwVugK1pFd+zyVk1L9F6ucmQ7oF2dpy8zKPHNlbW2zrU9Jst3fWYfG7z3owjxqhwAvi5G4o0AikrJMw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", - "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", - "license": "ISC" - }, - "node_modules/am-i-vibing": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", - "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "node_modules/astro/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-5N4DdzAmLtnyuh027H6ViC99BeoA4T0RwKS6qUrl6zY70AKRsNDrnEZeWnqrKmDRFxXmv8zf9yhF2DxN/SfyGQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "process-ancestry": "^0.1.0" - }, - "bin": { - "am-i-vibing": "dist/cli.mjs" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "node_modules/astro/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.3.0.tgz", + "integrity": "sha512-Tz+2b+yF5eVRGX3DWebn2JIvZkNYI1gkafI+nF2fau2zeQDOpBAS8w1PxHi4ke//NIOE6+2DNYyq1veIjzAXqQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">= 8" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/astro/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-ldH38lBlOpS+b4fbSG9euQjhYXLE18PIz5hyvf6gvFfyVLodRE/Tsu4jV8UmPqjjce6lotmUUwscxPuer4lczQ==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "license": "Apache-2.0", + "node_modules/astro/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-HglhUzUPuXNTGFt5ZwJalSEvavhFSl+nxpQM9urkzkt+2oGI1ZSwgyytk9aGw15/0kk1TCHPc57nv3DdiG8/yQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/astro": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.5.tgz", - "integrity": "sha512-1p/ernaq/RuESbM5NMjWzbO6SiebMGrGcYlGjMfBhzzgM+n/X7AUmVyw8BqW2QYCdivpsHwJBcD1fqmlTZjZHg==", + "node_modules/astro/node_modules/rolldown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.3.0.tgz", + "integrity": "sha512-7j1jdVlzWzbRat9EDMsB/QmyMcc6hOO0vbcma8LGjBTmKlpS5Hem+5fpntc/HoRV5tLrG+Qo6I1ZHV/ZuL5S8g==", "license": "MIT", "dependencies": { - "@astrojs/compiler-rs": "^0.5.0", - "@astrojs/internal-helpers": "0.11.0", - "@astrojs/markdown-satteri": "0.4.2", - "@astrojs/telemetry": "3.3.3", - "@capsizecss/unpack": "^4.0.0", - "@clack/prompts": "^1.1.0", - "@oslojs/encoding": "^1.1.0", - "am-i-vibing": "^0.4.0", - "aria-query": "^5.3.2", - "axobject-query": "^4.1.0", - "ci-info": "^4.4.0", - "clsx": "^2.1.1", - "common-ancestor-path": "^2.0.0", - "cookie": "^2.0.1", - "devalue": "^5.8.1", - "diff": "^9.0.0", - "dset": "^3.1.4", - "es-module-lexer": "^2.0.0", - "esbuild": "^0.28.0", - "find-proc": "0.2.0", - "flattie": "^1.1.1", - "fontace": "~0.4.1", - "get-tsconfig": "5.0.0-beta.4", - "github-slugger": "^2.0.0", - "html-escaper": "3.0.3", - "http-cache-semantics": "^4.2.0", - "js-yaml": "^4.3.2", - "jsonc-parser": "^3.3.1", - "magic-string": "^1.0.0", - "magicast": "^0.5.2", - "mrmime": "^2.0.1", - "neotraverse": "^1.0.1", - "obug": "^3.0.0", - "p-limit": "^7.3.0", - "p-queue": "^9.1.0", - "package-manager-detector": "^1.6.0", - "piccolore": "^0.1.3", - "picomatch": "^4.0.4", - "shiki": "^4.0.2", - "smol-toml": "^1.8.0", - "svgo": "^4.1.0", - "tinyclip": "^1.0.1", - "tinyexec": "^1.0.4", - "tinyglobby": "^0.2.15", - "ultrahtml": "^1.6.0", - "unifont": "~0.7.5", - "unstorage": "^1.17.5", - "verkit": "^0.4.0", - "vite": "^8.0.13", - "vitefu": "^1.1.2", - "xxhash-wasm": "^1.1.0", - "yargs-parser": "^22.0.0", - "zod": "^4.5.4" + "@oxc-project/types": "=0.157.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "astro": "bin/astro.mjs" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=22.12.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/astrodotbuild" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "sharp": "^0.35.4" - }, - "peerDependencies": { - "@astrojs/markdown-remark": "^7.3.0" - }, - "peerDependenciesMeta": { - "@astrojs/markdown-remark": { - "optional": true - } - } - }, - "node_modules/astro-expressive-code": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", - "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", - "license": "MIT", - "dependencies": { - "rehype-expressive-code": "^0.44.2", - "url-extras": "^0.1.0" - }, - "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" + "@rolldown/binding-android-arm-eabi": "1.3.0", + "@rolldown/binding-android-arm64": "1.3.0", + "@rolldown/binding-darwin-arm64": "1.3.0", + "@rolldown/binding-darwin-x64": "1.3.0", + "@rolldown/binding-freebsd-x64": "1.3.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.3.0", + "@rolldown/binding-linux-arm64-gnu": "1.3.0", + "@rolldown/binding-linux-arm64-musl": "1.3.0", + "@rolldown/binding-linux-ppc64-gnu": "1.3.0", + "@rolldown/binding-linux-s390x-gnu": "1.3.0", + "@rolldown/binding-linux-x64-gnu": "1.3.0", + "@rolldown/binding-linux-x64-musl": "1.3.0", + "@rolldown/binding-openharmony-arm64": "1.3.0", + "@rolldown/binding-wasm32-wasi": "1.3.0", + "@rolldown/binding-win32-arm64-msvc": "1.3.0", + "@rolldown/binding-win32-x64-msvc": "1.3.0" } }, "node_modules/axobject-query": { @@ -2686,10 +2873,13 @@ } }, "node_modules/devalue": { - "version": "5.9.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", - "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", - "license": "MIT" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-6.0.3.tgz", + "integrity": "sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/devlop": { "version": "1.1.0", @@ -2803,56 +2993,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", "license": "MIT" }, "node_modules/expressive-code": { @@ -2914,15 +3057,6 @@ } } }, - "node_modules/find-proc": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", - "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -2932,6 +3066,12 @@ "node": ">=8" } }, + "node_modules/fnv1a-64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", + "integrity": "sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==", + "license": "MIT" + }, "node_modules/fontace": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", @@ -2968,9 +3108,9 @@ } }, "node_modules/get-tsconfig": { - "version": "5.0.0-beta.4", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", - "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "version": "5.0.0-beta.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -3373,28 +3513,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsonc-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", @@ -3679,9 +3797,9 @@ } }, "node_modules/magic-string": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", - "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" @@ -4415,12 +4533,6 @@ "ufo": "^1.6.1" } }, - "node_modules/ohash": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", - "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", - "license": "MIT" - }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", @@ -4454,28 +4566,12 @@ } }, "node_modules/p-queue": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", - "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", - "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-10.0.0.tgz", + "integrity": "sha512-P2wrCri3bhGLPo3kX7qYVXbo/5GvaVATU0DpO24CbeT53Y4z35t8JUWku04ERpXHUYB5rPMi3TkWE3fmJgx5rA==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4555,9 +4651,9 @@ } }, "node_modules/postcss": { - "version": "8.5.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", - "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -4574,7 +4670,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.19", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" }, @@ -4608,9 +4704,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -4741,12 +4837,12 @@ } }, "node_modules/rolldown": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", - "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.152.0", + "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4756,21 +4852,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.12", - "@rolldown/binding-android-arm64": "1.2.12", - "@rolldown/binding-darwin-arm64": "1.2.12", - "@rolldown/binding-darwin-x64": "1.2.12", - "@rolldown/binding-freebsd-x64": "1.2.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", - "@rolldown/binding-linux-arm64-gnu": "1.2.12", - "@rolldown/binding-linux-arm64-musl": "1.2.12", - "@rolldown/binding-linux-ppc64-gnu": "1.2.12", - "@rolldown/binding-linux-s390x-gnu": "1.2.12", - "@rolldown/binding-linux-x64-gnu": "1.2.12", - "@rolldown/binding-linux-x64-musl": "1.2.12", - "@rolldown/binding-openharmony-arm64": "1.2.12", - "@rolldown/binding-win32-arm64-msvc": "1.2.12", - "@rolldown/binding-win32-x64-msvc": "1.2.12" + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" } }, "node_modules/satteri": { @@ -4913,9 +5009,9 @@ } }, "node_modules/smol-toml": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", - "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -4943,6 +5039,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -5022,6 +5124,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyps": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/tinyps/-/tinyps-0.5.1.tgz", + "integrity": "sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -5068,9 +5176,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", - "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "version": "8.12.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.4.tgz", + "integrity": "sha512-8Mx+uNKKER2bA4AdtedkqRbwUCIJFr2nJckXRe5yrDqzePqwLP7VNcctN8/XdmDSbF7mS+jJKIszlL+vbam1VA==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -5102,13 +5210,14 @@ } }, "node_modules/unifont": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", - "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-1.0.3.tgz", + "integrity": "sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", - "ohash": "^2.0.11", + "fnv1a-64": "^0.1.2", + "std-env": "^4.2.0", "undici": "^8.0.0" } }, @@ -5195,19 +5304,19 @@ } }, "node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.6.tgz", + "integrity": "sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==", "license": "MIT", "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", + "h3": "^1.15.11", + "lru-cache": "^11.5.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", - "ufo": "^1.6.3" + "ufo": "^1.6.4" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", @@ -5218,7 +5327,7 @@ "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", @@ -5309,9 +5418,9 @@ "license": "MIT" }, "node_modules/verkit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.4.1.tgz", - "integrity": "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", "license": "MIT", "engines": { "node": ">=18.12.0" @@ -5349,15 +5458,15 @@ } }, "node_modules/vite": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", - "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.11", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", "tinyglobby": "^0.2.17" }, "bin": { @@ -5374,7 +5483,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -5450,6 +5559,21 @@ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", @@ -5472,9 +5596,9 @@ } }, "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/documentation/package.json b/documentation/package.json index 6f657147..bcb5b286 100644 --- a/documentation/package.json +++ b/documentation/package.json @@ -12,10 +12,10 @@ "test": "node scripts/test-export.mjs" }, "dependencies": { - "@astrojs/starlight": "0.42.5", - "astro": "7.3.5" + "@astrojs/starlight": "0.42.9", + "astro": "7.4.7" }, "engines": { - "node": ">=22.12.0" + "node": ">=24.0.0" } } diff --git a/documentation/public/favicon.svg b/documentation/public/favicon.svg new file mode 100644 index 00000000..439fbcb5 --- /dev/null +++ b/documentation/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/documentation/scripts/check-output.mjs b/documentation/scripts/check-output.mjs index f34eab20..8337c1af 100644 --- a/documentation/scripts/check-output.mjs +++ b/documentation/scripts/check-output.mjs @@ -17,9 +17,13 @@ for (const slug of pages) { assert.ok(html.includes(`href="https://channelgate.dev${route}"`), `Missing canonical: ${route}`); assert.ok(html.includes('starlight'), `Missing Starlight layout: ${route}`); assert.ok(html.includes('aria-label="Main"'), `Missing persistent sidebar: ${route}`); + assert.ok(html.includes('href="/docs/favicon.svg"'), `Missing docs favicon link: ${route}`); assert.equal([...html.matchAll(/)/g)].length, 1, `Expected one page heading: ${route}`); htmlByPath.set(route, html); } +// Starlight emits this link by default, but does not provide the public asset. +const favicon = await readFile(resolve(dist, 'favicon.svg'), 'utf8'); +assert.match(favicon, /]*xmlns="http:\/\/www\.w3\.org\/2000\/svg"/); let internalLinks = 0; for (const [route, html] of htmlByPath) { for (const [, href] of html.matchAll(/]*\bhref="([^"]+)"/g)) { diff --git a/documentation/scripts/sync-content.mjs b/documentation/scripts/sync-content.mjs index ecad1025..190464ae 100644 --- a/documentation/scripts/sync-content.mjs +++ b/documentation/scripts/sync-content.mjs @@ -7,6 +7,7 @@ const docsRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const repository = resolve(docsRoot, '..'); const output = resolve(docsRoot, 'src/content/docs/generated'); const slugs = new Map(guides.map((guide) => [guide.source, guide.slug])); +slugs.set('documentation/src/content/docs/configuration/vps-sizing.md', 'configuration/vps-sizing'); const sourceRef = process.env.CHANNELGATE_DOCS_REF || 'beta'; const sourceBase = `https://github.com/makeitfutureDev/channelgate/blob/${encodeURIComponent(sourceRef)}/`; diff --git a/documentation/src/content/docs/configuration/access-and-users.md b/documentation/src/content/docs/configuration/access-and-users.md index b7ff7744..6febebfb 100644 --- a/documentation/src/content/docs/configuration/access-and-users.md +++ b/documentation/src/content/docs/configuration/access-and-users.md @@ -3,6 +3,9 @@ title: Access and users description: Approve users and separate conversation use, conversation management, and organization administration. --- +**Enterprise availability.** Exact company email-domain auto-approval requires an effective Enterprise license. Saved policies do not approve users after expiry; manual user approval remains available. + + ChannelGate checks who may talk to the bot before starting an agent run. Permission to use a conversation, permission to manage it, and organization administrator status are separate decisions. ## Approve a user diff --git a/documentation/src/content/docs/configuration/channel-settings.md b/documentation/src/content/docs/configuration/channel-settings.md index 78f51d4a..c86ad180 100644 --- a/documentation/src/content/docs/configuration/channel-settings.md +++ b/documentation/src/content/docs/configuration/channel-settings.md @@ -50,3 +50,19 @@ Most saved conversation changes apply on the next message. Secrets and Codex sig Gateway admins can manage conversation access. Delegated channel managers can use the Slack Access settings allowed by the management policy, but cannot make themselves organization admins. Runtime changes have a separate organization policy. Conversation skill assignments are available to members; organization templates remain admin-managed. See [Permissions](/docs/features/permissions), [Models and effort](/docs/features/models-and-effort), [Shared workspaces](/docs/features/shared-workspaces), and [Environment variables](/docs/configuration/environment-variables). + +## Monthly channel spending limit · Enterprise + +Set a positive USD amount for the conversation's monthly spending limit, or leave it unset for +no cap (`monthlySpendingLimitUSD: null`). The limit compares recorded spend in the current UTC +calendar month before admitting another run. Foreground, scheduled, API and background agent +launches use the same admission check. + +This is not a provider invoice cap. A running job can finish above the limit, and concurrent +launches can overshoot because spend is not reserved in advance. Unknown-cost records in the +current month block admission until pricing is repaired or an admin clears the cap. Usage and +costs outside this gateway are not controlled. + +Setting a limit requires Enterprise. A saved limit refuses new runs after downgrade instead of +silently removing the safety control; an admin can clear it without Enterprise. Existing recorded +usage is retained and the accounting month changes at midnight UTC on the first of the month. diff --git a/documentation/src/content/docs/configuration/deployment.md b/documentation/src/content/docs/configuration/deployment.md index c81ba355..2c35a818 100644 --- a/documentation/src/content/docs/configuration/deployment.md +++ b/documentation/src/content/docs/configuration/deployment.md @@ -5,9 +5,13 @@ description: Configure a Linux daemon, authenticated admin access, public URLs, ChannelGate's gateway is a self-hosted Linux daemon. The public website and documentation can be hosted separately on a static platform; that does not provide the gateway's container runtime, persistent storage, or engine processes. +## Recommended VPS resources + +Start with **4 dedicated vCPU, 16 GB RAM, and 150 GB or more of persistent SSD/NVMe** on Ubuntu 24.04 LTS, with `MAX_CONCURRENT_RUNS=4`. See [VPS sizing and hosting](/docs/configuration/vps-sizing) for pilot and busy-team tiers, monitoring, and backups. + ## Host prerequisites -Follow [Installation](/docs/installation) for the complete installer. The daemon needs Node.js **22.13 or later** and a usable container CLI. Rootless Podman is the preferred runtime; the daemon probes Podman and then Docker. Build the runtime image for the installed checkout before expecting agent turns to run. +Follow [Installation](/docs/installation) for the complete installer. The daemon needs Node.js **24.0.0 or later** and a usable container CLI. Rootless Podman is the preferred runtime; the daemon probes Podman and then Docker. Build the runtime image for the installed checkout before expecting agent turns to run. Choose the daemon's Linux account and persistent disk location before connecting your team. The optional systemd installer creates a dedicated non-login service account; it is a separate root-only operation. An interactive operator deployment and a dedicated service do not necessarily use the same home or engine login. diff --git a/documentation/src/content/docs/configuration/model-defaults.md b/documentation/src/content/docs/configuration/model-defaults.md index 7d1197fc..664692a0 100644 --- a/documentation/src/content/docs/configuration/model-defaults.md +++ b/documentation/src/content/docs/configuration/model-defaults.md @@ -15,6 +15,8 @@ Open **Settings → Agent defaults → Engine & runtime**. | Engine (which CLI drives the bot) | Default engine; Claude is the fallback default when none is configured | | Default Claude model | Sets the model used by inherited Claude runs | | Default Codex model | Sets the model used by inherited Codex runs | +| Default Claude / Codex effort | Sets reasoning effort when a conversation or thread does not override it | +| Provider default model and effort | Independent defaults on each optional Qwen provider card | | Automatic failover between harnesses | Enables supported Claude–Codex recovery before work starts; defaults on | | How a failover happens | Automatic, or ask in a live Slack thread; automatic is the default | @@ -22,18 +24,32 @@ Optional provider harnesses must be explicitly enabled and authenticated. At lea 1. Authenticate the engines you intend to use. 2. Disable engines the deployment cannot support. -3. Choose the default engine and an explicit model for each enabled engine when its controls offer one. +3. Choose the default engine, model, and supported effort for each enabled engine when its controls offer them. 4. Save, then create a fresh thread to check inherited behavior. An empty gateway model setting lets the CLI use its own default. An explicit gateway model helps avoid changes in the operator's interactive terminal model choice affecting gateway runs. +## Default model shortcuts + +New installations include `:astra`, `:sol`, `:terra`, `:fable`, `:opus`, and `:sonet` (Sonnet). +Use `@agent :sonet your task` in Slack to select and pin the current supported Sonnet model. +Before an administrator saves a custom shortcut list, targets follow the newest stable version +of each family in the gateway's model catalog. Codex uses its authenticated CLI catalog, with a +bundled fallback; Claude uses the curated catalog in the installed gateway version. Updating the +CLI or gateway is therefore still necessary when a provider exposes a model they do not yet list. +Unavailable families are omitted rather than assigned a guessed model ID. + +Edit shortcuts under **Settings → Access & security → Model shortcuts**. A saved list uses those +exact mappings until edited again; an empty list disables shortcuts. Existing customized lists and +thread pins survive updates. Saving unrelated settings does not turn inherited defaults into pins. + ## Conversation and thread overrides Set a conversation's **Engine**, **Model**, and **Effort** under **Conversations → Runtime**. On Slack, use `/model` to select a channel-wide or **just this thread** runtime. A thread-scoped choice does not change the default for other threads. For a new run, explicit per-run overrides take precedence, followed by thread selection, conversation/DM selection, and gateway defaults. Existing threads retain their owning engine session unless explicitly switched or recovered; changing the global default does not automatically migrate every active thread. -Effort is engine- and model-dependent. Use the offered choices rather than assuming all models support `xhigh` or the same reasoning settings. An empty effort value inherits the engine's default; it is not proof of a specific reasoning level. +Effort is engine- and model-dependent. Use the offered choices rather than assuming all models support `xhigh` or the same reasoning settings. An empty conversation or thread effort inherits the gateway's default effort for that harness. An empty gateway effort uses the CLI default; it is not proof of a specific reasoning level. Failover uses the destination harness's own gateway effort, checked against its selected model. ## Control who changes runtime diff --git a/documentation/src/content/docs/configuration/skill-sources.md b/documentation/src/content/docs/configuration/skill-sources.md index eb699cff..3ae08bda 100644 --- a/documentation/src/content/docs/configuration/skill-sources.md +++ b/documentation/src/content/docs/configuration/skill-sources.md @@ -3,6 +3,9 @@ title: Skill sources and templates description: Import, review, publish, and grant reusable skills and plugins. --- +**Enterprise availability.** Channel skill-template assignment and inherited template grants require an effective Enterprise license. Other tiers can continue assigning skills directly. + + The skills catalog stores reusable instructions and their supporting files as versioned revisions. Importing a skill makes it available in the catalog; granting it determines which conversations or people receive it. Templates collect skills into a reusable selection that conversations can follow. ## Add a source diff --git a/documentation/src/content/docs/configuration/vps-sizing.md b/documentation/src/content/docs/configuration/vps-sizing.md new file mode 100644 index 00000000..16e6f9ea --- /dev/null +++ b/documentation/src/content/docs/configuration/vps-sizing.md @@ -0,0 +1,75 @@ +--- +title: VPS sizing and hosting +description: Recommended VPS specifications, sizing tiers, concurrency, storage, and backups for a self-hosted ChannelGate gateway. +--- + +For a typical team deployment, start with **4 dedicated vCPU, 16 GB RAM, and at least 150 GB of persistent SSD/NVMe storage**, running **Ubuntu 24.04 LTS**. Start with four concurrent agent runs and adjust after observing your workload. + +These are planning recommendations, not measured capacity guarantees. Browser sessions, builds, background jobs, local transcription, and the number of active conversation containers change the resources you need. + +## Recommended production configuration + +| Component | Recommendation | +| --- | --- | +| CPU | 4 dedicated vCPU; prefer predictable allocation over heavily oversold shared cores | +| Memory | 16 GB RAM | +| Storage | At least 150 GB persistent SSD/NVMe | +| Operating system | Ubuntu 24.04 LTS, the tested distribution, with systemd | +| Container runtime | Rootless Podman; see [Installation](/docs/installation) | +| Network | Reliable outbound internet; a static public IPv4 is convenient for direct hosting | +| Swap | 4–8 GB as a buffer; size RAM for the active workload | +| Concurrent agent runs | Start with `MAX_CONCURRENT_RUNS=4` | +| Backups | Provider snapshots plus encrypted off-host backups; verify a restore before relying on them | +| GPU | Not required for hosted Claude or Codex inference | + +The AI models run at the engine provider. Your VPS runs the gateway, conversation containers, coding tools, browsers, and any local processing you enable. Hosting costs are separate from engine subscriptions or API charges. + +## Choose a sizing tier + +| Deployment | CPU | RAM | Persistent SSD/NVMe | Starting concurrency | +| --- | --- | --- | --- | --- | +| Pilot or light use | 2 vCPU | 8 GB | 50 GB | 2 | +| Typical production team | 4 dedicated vCPU | 16 GB | 150 GB or more | 4 | +| Busy team | 8 vCPU | 32 GB | 200 GB or more | 6–8 | + +A pilot configuration suits a small trial with limited simultaneous work. For production involving browser automation or several active containers, use the 16 GB tier as your starting point. Large builds, many browser sessions, and local transcription can require more RAM or disk even with fewer concurrent runs. + +Concurrency counts simultaneous agent runs, including background agent runs, rather than registered users or conversations. Background shell jobs and other processes can consume resources outside that cap. + +## Set concurrency and monitor usage + +The gateway defaults to **8** concurrent runs. For the recommended 16 GB host, set the startup environment to: + +```dotenv +MAX_CONCURRENT_RUNS=4 +``` + +Set this in the gateway's service/startup environment, rather than a conversation's secrets, and restart the service safely. For the pilot tier, start at `2`. + +Use [System health](/docs/features/system-health) to watch CPU, memory, swap, and storage during representative busy periods. Reduce concurrency if memory pressure or swapping grows; increase resources before raising concurrency when the host is already constrained. Sustained swap activity is a signal to revisit capacity. + +## Choose a hosting provider + +Choose a Linux VPS, dedicated server, or physical host that supports **systemd, persistent local storage, and rootless containers**. Check the provider's virtualization and container support before ordering. Dedicated CPU allocation is useful when several conversations run builds or browsers at once. + +Compare the complete cost of compute, disk, IPv4, backups, and outbound traffic, as well as the region and support you need. Provider plans and prices change; verify the current offer directly with the provider. A hyperscaler VM can work if it meets the same requirements. + +Static hosting and serverless application platforms can host the public website or documentation. The gateway itself needs an always-on Linux host with its own container runtime and durable state. + +A public IPv4 is a practical hosting choice, rather than a requirement for Slack Socket Mode. Public HTTPS reachability is needed for Microsoft Teams messaging and externally accessible links or webhooks; see [Deployment configuration](/docs/configuration/deployment) for the relevant integrations and tunnel/proxy options. + +## Contabo option + +[Contabo](https://contabo.com/) is a budget alternative. Its [current VPS documentation](https://docs.contabo.com/docs/servers-hosting/vps/) lists **Cloud VPS 8: 8 shared vCPU, 24 GB RAM, and 300 GB SSD** (checked 2026-11-03). + +This meets our memory and disk starting points, with shared CPU allocation. Start at `MAX_CONCURRENT_RUNS=2` and raise after measuring your workload. Compare Contabo’s Max Performance VPS (VDS) family if you need dedicated cores. These are published specifications; we have not benchmarked ChannelGate on this plan. + +Check [Contabo’s current pricing](https://contabo.com/en/pricing/) for your region, contract term, tax, setup fees, and backup add-ons before ordering. Choose Ubuntu 24.04 LTS and follow the same installation and off-host backup guidance as for other providers. + +## Plan for storage and recovery + +Allow room for runtime images, conversation workspaces and HOME volumes, browser assets, uploaded files, build artifacts, logs, and backups. The disk recommendations are starting capacities; retained project files and media can outgrow them. + +Keep off-host backups of the gateway state and the persistent conversation storage you need to recover. Provider snapshots are an additional recovery layer. Follow [Backup and restore](/docs/features/backup-and-restore), [Storage maintenance](/docs/features/storage-maintenance), and the [Operations runbook](/docs/operations) for coverage and restore procedures. + +Once the host is ready, follow [Install ChannelGate](/docs/installation) and [Deployment configuration](/docs/configuration/deployment). diff --git a/documentation/src/content/docs/controls.mdx b/documentation/src/content/docs/controls.mdx index 5c44f2ce..abed018d 100644 --- a/documentation/src/content/docs/controls.mdx +++ b/documentation/src/content/docs/controls.mdx @@ -9,7 +9,7 @@ import { controlPages } from '../../data/control-reference.mjs'; Ask the agent for an outcome in ordinary language. It can use the controls below when its current permissions, platform, and connections allow them. These reference pages explain the exact tool names, inputs, results, and restrictions for people building integrations or checking how an action works. -The gateway defines **110 controls**. The separate [skills-library service](/docs/controls/skills-library) defines **10 more**. A run receives an authorized subset; a tool appearing here does not mean it is enabled in every conversation. The library endpoint uses its own access tokens. +The gateway defines **131 controls**. The separate [skills-library service](/docs/controls/skills-library) defines **10 more**. A run receives an authorized subset; a tool appearing here does not mean it is enabled in every conversation. The library endpoint uses its own access tokens. For practical workflows, browse the [feature directory](/docs/features). For deployment and conversation defaults, open [configuration](/docs/configuration). The [functionality map](/docs/functionality) connects the broader product areas to their guides. diff --git a/documentation/src/content/docs/controls/accounts-and-secrets.md b/documentation/src/content/docs/controls/accounts-and-secrets.md index ef914bfe..cfa25981 100644 --- a/documentation/src/content/docs/controls/accounts-and-secrets.md +++ b/documentation/src/content/docs/controls/accounts-and-secrets.md @@ -1,6 +1,6 @@ --- title: Account and secret controls -description: Nine controls for personal connected accounts and scoped environment credentials. +description: Seventeen controls for connected accounts, email, and scoped environment credentials. --- Credential tools change saved account or environment state. Prefer the private UI fields so secrets never enter chat history. No tool on this page reveals a stored secret value. Process environment availability and connected-account identity are separate concepts. @@ -156,6 +156,82 @@ An email account is a mailbox's IMAP and SMTP settings entered once on one form **Restrictions:** sending is refused on an account whose *allow sending* is off. Replies, streams and job output are redacted of every stored password. +## list_email_accounts + +**Arguments:** none. + +**Scope and result:** Lists available channel and verified requester's personal accounts, their names, addresses, scopes, mail servers and whether sending is enabled. Never returns passwords. Connect accounts through the private settings forms described above. + +**Limits:** Personal results in a shared channel are visible to that thread's audience; explain this before reading personal mail. HTTP run-API attribution does not grant access to personal accounts. + +## email_mailboxes + +**Optional:** `account` (account selector described above; optional only when exactly one account is available). + +**Scope and result:** Lists selectable folders and their special roles for the chosen account. + +## email_search + +**Optional:** `account`, `mailbox` (defaults to `INBOX`), `query`, `from`, `to`, `subject`, `since` (inclusive ISO date), `before` (exclusive ISO date), `unseen`, `flagged`, and `limit` (integer 1–50; default 20). + +**Scope and result:** Returns matching message UIDs, dates, senders, subjects, unread/flag marks and attachment counts, newest first. Filters combine with AND; omit filters to list recent messages. Use the UID with the same account and mailbox in subsequent controls. + +## email_read + +**Required:** `uid` (positive integer from search). **Optional:** `account`, `mailbox` (default `INBOX`), `max_chars` (integer 500–100,000; default 20,000). + +**Scope and result:** Returns headers, text body (plain text preferred, HTML converted), and attachment part IDs. Reports truncated bodies; raw body reads are bounded to 2 MiB. + +## email_download_attachment + +**Required:** `uid` (positive integer), `part` (part ID from `email_read`, such as `2` or `1.2`). **Optional:** `account`, `mailbox` (default `INBOX`). + +**Scope and result:** Saves one attachment under `uploads/email//` in the current working folder and returns its relative path, size and content type. + +**Limits:** 50 MiB per attachment; confined no-follow writes. Read the returned file or share it through the current conversation's file tool. + +## email_move + +**Required:** `uid` (positive integer), `to` (destination mailbox path from `email_mailboxes`). **Optional:** `account`, `mailbox` (source; default `INBOX`). + +**Scope and result:** Moves the message, returning source, destination and the new UID when the server provides one. Use for archiving, trash or filing only within the selected account. + +## email_set_flag + +**Required:** `uid` (positive integer), `flag` (`seen`, `unseen`, `flagged`, `unflagged`, `answered`). **Optional:** `account`, `mailbox` (default `INBOX`). + +**Scope and result:** Changes the selected message's flag and returns a confirmation identifying account, mailbox and UID. + +## email_send + +**Required:** `to` (nonempty array of recipient strings), `subject`, `text` (plain-text body). **Optional:** `account`, `cc`, `bcc` (recipient arrays), `html` (HTML alternative), `attachments` (up to 10 working-folder-relative paths), `in_reply_to` (original Message-ID for threading), `reply_to` (Reply-To header). + +**Scope and result:** Sends from the connected account's address, returns accepted and rejected recipients and Message-ID when available, and reports whether a Sent copy was saved. Names such as `Name ` are accepted as recipient strings. + +**Limits:** Sending must be enabled for the selected account. Attachments must be confined to the working folder and total at most 25 MiB. Confirm recipients and content unless the user already supplied them; personal accounts send as that person. A Sent copy depends on the server exposing a Sent mailbox. + ## Related guides [Accounts](/docs/features/connected-accounts), [environment configuration](/docs/configuration/environment-variables), [secrets](/docs/features/secrets), [Composio SDK](/docs/features/composio-sdk), and [Toolbox/Make](/docs/features/toolbox-and-make). + +## create_api_token + +**Required:** `name` (string label). + +**Scope and result:** Creates a personal HTTP run API token bound to this conversation. Runs act as the token owner with their current role, conversation authorization, and personal connections. Returns the token value once with an example request; subsequent listings never reveal it. Runs appear as threads in this conversation. + +**Restrictions:** Requires a trusted human requester. The token stops working when revoked or when its owner loses access. Store the returned value securely. + +## list_api_tokens + +**Inputs:** None. + +**Scope and result:** Lists this requester's HTTP run API tokens in the current conversation. A channel manager or administrator can list all tokens here. Returns IDs, labels, status, and last four characters, never token values. Requires a trusted human requester. + +## revoke_api_token + +**Required:** `id` (integer token ID from `list_api_tokens`). + +**Scope and result:** Revokes a token in the current conversation. Its next API request is refused with HTTP 401. Repeated revocation reports that it is already revoked. + +**Restrictions:** Requires a trusted human requester who owns the token, manages this channel, or is an administrator. Cannot revoke a token belonging to another conversation. diff --git a/documentation/src/content/docs/controls/agent-interaction.md b/documentation/src/content/docs/controls/agent-interaction.md index 9d208666..64b657ec 100644 --- a/documentation/src/content/docs/controls/agent-interaction.md +++ b/documentation/src/content/docs/controls/agent-interaction.md @@ -35,6 +35,21 @@ These gateway MCP tools use the signed current-run conversation and author conte **Limits:** The agent starts without the parent conversation transcript, so task scope, inputs, and deliverable must be explicit. End the launching turn immediately. The maximum agent run is one week. Successful reports use the normal sanitized, chunked message delivery. Restarted unfinished agents are recorded as interrupted; they are not a promise of uninterrupted process survival. +## send_agent_message + +**Required:** `to` (string, the `channelId` or exact name a `search_agents` result returned), `message` (string, a self-contained question or request of up to 8,000 characters). + +```json +{ + "to": "C0123456789", + "message": "What deployment steps does your channel use for the staging environment? A short ordered list is enough." +} +``` + +**Scope and result:** Sends a question or request to another channel's agent on behalf of the requesting person, after [search_agents](/docs/controls/knowledge-and-instructions/#search_agents) found a suitable target. The call posts a confirmation card in the current thread; when the requesting person approves it, the daemon delivers the text as a new thread in the target channel, that channel's agent answers there with its own tools and permissions, and the reply is posted back into the originating thread as a new turn. Returns a sent acknowledgement (or a refusal naming the gate that failed). + +**Limits:** Only an authenticated conversation member with current channel access can send; API runs and messages delivered by another agent are refused. The requester must be authorized and a current member of the target channel, everyone who can read the originating conversation must belong to it too, and the target's manager must have switched `acceptsMessages` on (see [set_channel_agent_profile](/docs/controls/conversation-settings/#set_channel_agent_profile)). Text only; one message in flight per thread and channel, rate-limited per hour. The sending agent ends its turn after the call instead of waiting for the reply. + ## request_approval **Required:** `details` (string). **Optional:** `title` (defaults to “Approval requested”), `approve_label` (defaults to “Approve”), `deny_label` (defaults to “Deny”). diff --git a/documentation/src/content/docs/controls/automation.md b/documentation/src/content/docs/controls/automation.md index 18fa0a68..275f36cc 100644 --- a/documentation/src/content/docs/controls/automation.md +++ b/documentation/src/content/docs/controls/automation.md @@ -76,7 +76,7 @@ Example arguments: Returns JSON with `summary` and newest-first `runs`, or a single run for `run_id`. For older pages, use the last returned run's ID as `before`. A missing or foreign `run_id` returns an error; an unknown cursor returns an empty list. -History retains completed runs for 90 days, without historical backfill. It includes scheduling delay, execution and delivery status, engine/model, duration, recorded tokens/cost, a bounded summary and connection outcomes. `engineStatus` describes engine completion; `taskStatus` and `outcomeSource` distinguish agent-reported results from gateway facts. A completed engine is not proof that the external task succeeded. Missing or invalid task reports remain `unreported`. +History retains completed runs for 90 days, without historical backfill. Free structured costs cover only the last 30 days; older costs are hidden, while operational records remain. `summary.costHistoryDays` states the cost window separately from `retentionDays`. It includes scheduling delay, execution and delivery status, engine/model, duration, recorded tokens/cost, a bounded summary and connection outcomes. `engineStatus` describes engine completion; `taskStatus` and `outcomeSource` distinguish agent-reported results from gateway facts. A completed engine is not proof that the external task succeeded. Missing or invalid task reports remain `unreported`. The bounded event trail records lifecycle events, tool names and completion/failure statuses, with a dropped-event count when truncated. It does not retain tool arguments, raw outputs, reasoning or credential payloads. Deleting a schedule preserves its run history within retention. diff --git a/documentation/src/content/docs/controls/conversation-settings.md b/documentation/src/content/docs/controls/conversation-settings.md index 8082519b..ede842c9 100644 --- a/documentation/src/content/docs/controls/conversation-settings.md +++ b/documentation/src/content/docs/controls/conversation-settings.md @@ -264,3 +264,11 @@ Example arguments: - [Drive sync](/docs/features/drive-sync) - [Updates and rollback](/docs/features/updates) - [Restart and recovery](/docs/features/restart-and-recovery) + +## set_channel_skill_swap + +**Required:** `enabled` (boolean). + +**Scope and result:** Enables or disables preferred engine/model selection from a skill's `model:` field in this conversation. Default is off. When enabled, a skill can resume the same session on a model of the current engine or delegate to a background agent on another engine. A person's `/model` pin takes precedence. Effective on the next `use_skill` call. + +**Restrictions:** Requires a channel manager or administrator and an already configured conversation. Does not change other conversations. diff --git a/documentation/src/content/docs/controls/skills.md b/documentation/src/content/docs/controls/skills.md index f0c0f7b1..a01b3a69 100644 --- a/documentation/src/content/docs/controls/skills.md +++ b/documentation/src/content/docs/controls/skills.md @@ -460,3 +460,31 @@ Synchronize configured sources now. ## Related guides [Skill discovery](/docs/features/skill-discovery), [grants](/docs/features/skill-grants), [authoring](/docs/features/skill-authoring), [sources](/docs/features/skill-synchronization), and [usage](/docs/features/skill-usage). + +## use_skill + +**Required:** `name` (exact skill name/slug from its `SKILL.md`). + +**Scope and result:** Evaluates a skill's preferred engine/model when the agent starts following a skill that requests this call. The result directs the agent to continue, stop so the same session resumes on the preferred model, or delegate to a background agent on another engine. Follow the returned direction literally. + +**Restrictions:** Not for merely reading or comparing skills. Uses this live conversation and thread. Without live context or an available skill-swap service, the agent continues on its current model. Conversation swapping policy and the user's model pin still apply. + +## create_my_skill_token + +**Required:** `name` (string label, such as `work laptop`). + +**Scope and result:** Creates a personal token for the skill catalog's MCP endpoint and returns its value once with client connection instructions. It acts as the requester: organization skills, skills of channels that admit them, and their personal skills. Read, proposal, and edit rights follow the same access rules; library export is unavailable. + +**Restrictions:** Requires an approved person acting from their own message, never an HTTP API run. At most five active tokens per person. Copy the value securely; it cannot be shown again. + +## list_my_skill_tokens + +**Inputs:** None. + +**Scope and result:** Lists the approved requester's personal skill catalog token IDs, names, prefixes, creation/last-use dates, and revoked status. Never returns token values. + +## revoke_my_skill_token + +**Required:** `id` (integer from `list_my_skill_tokens`). + +**Scope and result:** Revokes one of the approved requester's own personal skill catalog tokens. Revocation takes effect on the token's next request. Cannot revoke another person's token. diff --git a/documentation/src/content/docs/controls/slack.md b/documentation/src/content/docs/controls/slack.md index 9f398cfa..4eb6fd83 100644 --- a/documentation/src/content/docs/controls/slack.md +++ b/documentation/src/content/docs/controls/slack.md @@ -235,3 +235,27 @@ These tools use the gateway’s Slack bot token and trusted current conversation **Scope and result:** Asks Slack for a file descriptor, proves sharing in the current conversation, and downloads into this thread’s uploads folder. Returns a local path, MIME type when available, and size. A existing confined download is reused. **Limits:** At most 500 MiB per attachment. The bot must have file access and Slack must report the file shared here. It returns no private URL/token. Download refusals and unavailable metadata do not count as content inspection. + +## open_code_channel + +**Required:** `task` (3–2,000 characters). **Optional:** `name` (up to 80 characters), `mode` (`auto`, `worktree`, `in_place`; default `auto`), `private` (boolean; defaults to origin channel visibility). + +**Scope and result:** Requests a dedicated Slack code channel for an explicitly requested task. A human must approve the card before creation. It inherits the origin's folder, access, secrets and skills; `auto` uses a task branch in a git worktree when the folder is a repository, otherwise its own files. Returns the channel and work location; starts the first turn when available. + +**Limits:** Starts from a live message in a regular channel, never a DM, another code channel, scheduled run or background run. The Code tab shows the task diff; teammates can join the dedicated channel. + +## update_code_channel + +**Optional:** `title` (up to 200 characters), `context_items` (up to five objects with `key` up to 64, `label` up to 128, optional `icon` and `url`), `view`, `refresh_diff` (boolean). Supply at least one change. + +**Scope and result:** Updates the current code channel's title, context bar, artifact tab or Code diff. `view.type` is `html`, `block_kit`, `canvas` or `pull_request`; optional `key` (up to 255 characters) identifies a replaceable tab and `name` (up to 80) labels it. Supply `content` for a self-contained HTML preview, `blocks` (up to 50) for Block Kit, `canvas_id` for a canvas, or `pr_url` for an HTTPS pull request. Returns per-change success or error messages. + +**Limits:** Only an active code channel. Context items merge with defaults, with up to five items total. HTML renders sandboxed. Diff refresh normally happens after each turn; `refresh_diff:true` forces a refresh. + +## archive_code_channel + +**Optional:** `summary` (up to 3,900 characters). + +**Scope and result:** After the requester agrees and a human approves the card, posts the wrap-up, shares it to the origin thread, closes the session and archives the current code channel. + +**Limits:** Only a code channel. History and artifact tabs remain readable and searchable; the task branch remains on disk. An already archived channel returns a notice. diff --git a/documentation/src/content/docs/controls/ssh.md b/documentation/src/content/docs/controls/ssh.md index 63dcc5e0..46df2d03 100644 --- a/documentation/src/content/docs/controls/ssh.md +++ b/documentation/src/content/docs/controls/ssh.md @@ -81,3 +81,17 @@ SSH gives a person a full interactive shell inside a conversation’s shared con **Scope and result:** Read-only connection/status view for a requester authorized in this conversation. Returns host setup state, granted people, registered-key readiness, live sessions, the SSH config block, and the working-folder/VS Code command when setup permits. **Limits:** A displayed snippet is not a access grant. The broker rechecks the key ownership, conversation authorization/grant, and operator-home restriction on connection. The container stays leased while a SSH session is live; daemon restart drops it. All people in the container share its operating-system user, so set Git identity and coordinate edits. + +## allow_channel_ssh_host + +**Required:** `host` (one exact public DNS hostname or IPv4/IPv6 address, without a URL, wildcard or port). + +```json +{ + "host": "ssh.example.com" +} +``` + +**Scope and result:** Requests a durable human approval card for outbound SSH from this conversation's container. Any currently authorized conversation member with write access may approve; manager/admin rights and `/sudo` are not required. The gateway appends the approved hostname or IP address to this conversation's `egressRawHosts`, preserving other entries and settings. An existing exact entry needs no additional card. + +**Limits:** Network must be on and the conversation must permit writes. Auto mode never approves the card. The saved request survives restarts and applies once; the requester and approver must still be authorized when it is decided. The card discloses that the existing egress list permits raw TCP ports 22, 5432 and 6543. Private, loopback and metadata addresses remain blocked, including after DNS changes. Approval supplies no SSH key or secret permission. Once approved, retry SSH with the existing key and `-o ProxyCommand='/opt/channelgate/bin/cg-egress-connect %h %p'`. diff --git a/documentation/src/content/docs/features/admin-dashboard.md b/documentation/src/content/docs/features/admin-dashboard.md index 9e8cdb59..493dd0a6 100644 --- a/documentation/src/content/docs/features/admin-dashboard.md +++ b/documentation/src/content/docs/features/admin-dashboard.md @@ -7,10 +7,18 @@ The admin website runs as part of your ChannelGate installation. It manages the ## Sign in -By default, the daemon serves the interface on `http://localhost:4747`. An operator can change the port or put a trusted tunnel/reverse proxy in front of it. On first boot without a configured password, ChannelGate generates an admin password once and stores its hash. Use the password supplied by the installation's operator. +By default, the daemon serves the interface on `http://localhost:4747`. An operator can change the port or put a trusted tunnel/reverse proxy in front of it. On first boot without a configured password, ChannelGate generates an admin password once and stores its hash. Use the password supplied by the installation's operator for initial setup, then verify and link your existing gateway administrator user. Normal sign-in uses a passkey, with optional password and Slack code sign-in as described below. A public deployment should set **Settings → Connection → Public URL** to its reachable HTTPS origin. This also gives approval links a reachable address. Setting Public URL does not create a tunnel or open a firewall. +## Optional password and Slack code sign-in + +A linked Slack administrator can enable **Settings → Admin access → Password and Slack code** after a fresh sign-in. It is off by default. Keep the current shared admin password or set a new one of at least 12 characters. During initial setup, verify and link the administrator first, then use this option to complete setup without creating a passkey. + +On the sign-in page, choose **Sign in with password and Slack code**, enter the shared admin password and your Slack email address, then enter the eight-digit code delivered privately by the gateway bot. The gateway resolves that email through Slack using its app permission `users:read.email`, then keeps the existing Slack user ID as the account identity. Adding that scope requires reinstalling the Slack app. Codes expire after five minutes, work only in the requesting browser, and allow at most five incorrect guesses. This option requires a current gateway Slack administrator and working private Slack delivery; approved members alone cannot use it. Passkeys remain available. + +Changing the shared password or disabling this option revokes existing password-and-code sessions. Removing the user's administrator role also revokes access. Disabling it while signed in this way signs out that browser; retain another usable sign-in method before disabling it. + ## Choose the right page - **[Overview](/docs/features/usage-and-costs):** run, token, cost, model, user, conversation, and skill summaries. @@ -31,7 +39,7 @@ Settings display inherited and explicit values where supported. Blank password/t Platform connections can be connected or reconnected without restarting the whole daemon. Runtime/image changes may affect future container starts; inspect the resulting status and verify a real task after a change. -Chat admins and the password-authenticated web session are distinct ways of authorizing control actions. Being an approved chat user does not itself provide the admin website password. +Dashboard sessions are linked to an existing gateway administrator user. Being an approved chat user alone does not grant dashboard access. The shared password alone cannot grant a full dashboard session. ## Related guides diff --git a/documentation/src/content/docs/features/conversation-forks.md b/documentation/src/content/docs/features/conversation-forks.md index 11c8705c..18c9f888 100644 --- a/documentation/src/content/docs/features/conversation-forks.md +++ b/documentation/src/content/docs/features/conversation-forks.md @@ -13,7 +13,7 @@ Inside the source Slack thread, send: @ChannelGate /fork Explore an alternative using a queue-based import worker. ``` -ChannelGate uses the selected engine's native fork command, creates a new top-level Slack message linking to the source, posts the new request in that thread, and runs it with a distinct child session ID. The source thread receives a backlink. +ChannelGate uses the selected engine's native fork command, creates a new top-level Slack message showing your new request above the `↪️ Fork of this thread` source link, posts the new request in that thread, and runs it with a distinct child session ID. The source thread receives a backlink. The source retains its own session ID and history. Engine, model, effort, and thread Clean pins carry into the branch. Subsequent requests in either thread continue their respective sessions. diff --git a/documentation/src/content/docs/features/dedicated-codex-login.md b/documentation/src/content/docs/features/dedicated-codex-login.md index d62945f3..bbaf5b96 100644 --- a/documentation/src/content/docs/features/dedicated-codex-login.md +++ b/documentation/src/content/docs/features/dedicated-codex-login.md @@ -3,6 +3,9 @@ title: A dedicated Codex login for a conversation description: Select a separate Codex account without falling back to another conversation or gateway login. --- +**Enterprise availability.** Dedicated channel Codex authentication requires an effective Enterprise license. A configured dedicated-login channel refuses a run after downgrade rather than silently using the shared gateway account. Shared gateway Codex authentication remains available in other tiers. + + A conversation can use the gateway's Codex connection or its own dedicated Codex login. This separates the authentication source for that conversation while leaving Slack and Claude on their existing gateway connections. ## Select the authentication source diff --git a/documentation/src/content/docs/features/drive-sync.md b/documentation/src/content/docs/features/drive-sync.md index 1d382979..4ea0e5c1 100644 --- a/documentation/src/content/docs/features/drive-sync.md +++ b/documentation/src/content/docs/features/drive-sync.md @@ -3,6 +3,9 @@ title: Google Drive workspace sync description: Link a Drive folder to a conversation workspace with scheduled and on-demand two-way synchronization. --- +**Enterprise availability.** Google Drive workspace sync requires an effective Enterprise license, for both scheduled and on-demand passes. Saved configuration and files survive expiry; synchronization does not continue without Enterprise. + + Drive sync keeps a conversation's working folder synchronized with a selected Google Drive folder. It uses the installation's Workspace service account and `rclone bisync`, separately from an agent's personal/shared Composio Drive tools. ## Configure the installation diff --git a/documentation/src/content/docs/features/licensing.md b/documentation/src/content/docs/features/licensing.md index 32a8e2f4..d1ddd5c8 100644 --- a/documentation/src/content/docs/features/licensing.md +++ b/documentation/src/content/docs/features/licensing.md @@ -17,15 +17,27 @@ Keys belong to the organization operating the installation. A service provider i The published defaults are: -| State | Conversations | AI runs per conversation per UTC month | -| --- | --- | --- | -| No key | 1 | 500 | -| Free key | Unlimited | 500 | -| Enterprise key | Unlimited | Unlimited | - -The issued key's verified limits are authoritative; limits may change under the published terms. Without a key, the first conversation admitted in a calendar month is the one served that month. - -An AI message means an engine run started, including user tasks, follow-ups, scheduled runs, and background work. The gateway's memory-review runs are excluded. It warns at 80% of a monthly cap; at the cap it posts a visible refusal instead of spawning a run. +| State | Registered channels | Active conversations per channel per UTC month | AI runs per channel per UTC month | +| --- | --- | --- | --- | +| No key | Up to 10; one active channel per month | 100 in the active channel | 500 | +| Free key | Up to 10 | 100 in each channel | 500 | +| Enterprise key | Unlimited by default | Unlimited by default | Unlimited by default | + +A conversation in the 100 allowance is an active **chat thread within a channel**, not an +individual AI run. Free installations also allow up to 10 stored human users. Every channel has +its own monthly thread allowance; repeated messages in an admitted thread reuse its slot. The +500-run monthly allowance is shared by the whole channel. Signed smaller limits are honored; +older Free keys cannot exceed these ceilings. Enterprise defaults to unlimited. + +Without a key, the first active channel admitted in a UTC month is the one served that month. +Thread tracking starts with post-upgrade activity, without inventing earlier thread history; +old threads count when next used. Already admitted threads remain usable through a stricter +limit or downgrade for the rest of the month, subject to the channel's AI-run allowance. The +thread set resets at the next UTC month boundary. + +An AI run includes user tasks, follow-ups, schedules and background work. Memory-review runs +are excluded. At a cap the gateway posts a visible refusal instead of spawning an engine; the +existing AI-run allowance warns at 80%. ## Offline and privacy @@ -41,3 +53,35 @@ Core confinement, permissions, memory, skills, standard chat connectors, and bac - [Licensing FAQ](/docs/licensing/faq) - [Privacy](/docs/privacy) - [Usage and costs](/docs/features/usage-and-costs) + +## Cost-tracking history + +Free installations (with or without a key) expose the most recent 30 days of cost-tracking history. Enterprise exposes unlimited history. This reporting window applies to audit records, summaries and dashboard totals, including external engine usage. Structured costs in audit events, HTTP run-job responses and schedule telemetry follow the same window; operational records and answer text remain available. It does not delete stored accounting, alter monthly AI allowances, or limit conversation history. + + +## Enterprise organization controls + +The following capabilities require an effective Enterprise license (including use of previously +saved configuration after downgrade or expiry): + +- Skill preferred-model switching: opt a conversation into honoring a skill's engine/model preference. +- Exact company email-domain auto-approval for new users and policy application to existing users. +- A dedicated Codex login per conversation, separate from the gateway login. +- The external skill-catalog MCP endpoint and its catalog/user access tokens. +- Organization environment variables shared across conversations. +- Google Drive workspace synchronization, both scheduled and on demand. +- Channel skill templates and their inherited skill grants. +- Per-channel monthly USD spending limits. + +Other tiers retain ordinary skills, channel/personal environment variables, the shared gateway +Codex login, manual user approval and standard chat connectors. Stored Enterprise configuration +is preserved when an entitlement expires; it does not activate Enterprise behavior. A channel +configured with a dedicated Codex login is refused rather than silently switched to another account. + +A spending limit is an admission control against the channel's recorded spend in the current UTC +calendar month. It is not a provider billing limit: an in-flight run can take recorded spend above +the cap, concurrent runs can overshoot, and provider-side costs are not reserved in advance. +`monthlySpendingLimitUSD: null` means no cap; a configured cap is a positive finite USD amount. +A current-month usage row with unknown cost refuses new runs until pricing is repaired or an +admin clears the cap. A saved cap also refuses new runs after downgrade; an admin can clear it +without Enterprise, so the safety control never silently disappears. diff --git a/documentation/src/content/docs/features/operator-commands.md b/documentation/src/content/docs/features/operator-commands.md index 0848faf4..70da7166 100644 --- a/documentation/src/content/docs/features/operator-commands.md +++ b/documentation/src/content/docs/features/operator-commands.md @@ -16,7 +16,7 @@ These commands run from the **gateway source checkout**, as its configured OS ac | `npm run update` | Host-managed transactional update | | `npm run build:image` | Build the managed container image | -Read [installation](/docs/installation) and [operations](/docs/operations) for prerequisites, service permissions, and exact deployment setup. ChannelGate requires Linux and Node 22.13 or newer; ordinary engine turns need the supported container runtime/image. +Read [installation](/docs/installation) and [operations](/docs/operations) for prerequisites, service permissions, and exact deployment setup. ChannelGate requires Linux and Node 24.0.0 or newer; ordinary engine turns need the supported container runtime/image. ## Data and capacity diff --git a/documentation/src/content/docs/features/secrets.md b/documentation/src/content/docs/features/secrets.md index 3f803389..ad880a77 100644 --- a/documentation/src/content/docs/features/secrets.md +++ b/documentation/src/content/docs/features/secrets.md @@ -3,6 +3,9 @@ title: Environment secrets description: Provide scoped CLI credentials without pasting them into chat. --- +**Enterprise availability.** Organization environment variables require an effective Enterprise license and are excluded from new runtime environments after expiry. Channel and personal environment variables remain available in other tiers with their normal precedence and isolation. + + Environment secrets make credentials available to permitted tools during a run. They are separate from Composio connected accounts and separate from the gateway's engine authentication settings. ## Choose the right scope diff --git a/documentation/src/content/docs/features/shared-folder-conflicts.md b/documentation/src/content/docs/features/shared-folder-conflicts.md index c76e0480..0b648ad4 100644 --- a/documentation/src/content/docs/features/shared-folder-conflicts.md +++ b/documentation/src/content/docs/features/shared-folder-conflicts.md @@ -16,13 +16,17 @@ A Git branch alone does not isolate working files. Use a distinct worktree for e The gateway's per-thread queue orders requests within that thread. It does not lock every project file against work in another thread or an authorized developer session. -## Align conversations sharing one folder +## Linked folder setup -A working folder also contains gateway-managed instructions, shared skill materialization, and portable memory files. Two conversations pointed at that folder cannot safely impose incompatible managed selections. +Selecting a folder already assigned to another conversation links to that conversation's shared setup automatically. The established folder owner remains the source even when an older conversation joins later. Existing duplicate assignments resolve to the first stored conversation. -ChannelGate checks compatible shared skill grants and memory settings before workspace synchronization. A conflict refuses provisioning with guidance to separate the folders or align the selections. It does not silently replace the other conversation's managed setup. The conflict notice does not disclose another conversation's identity or private folder path. +Skills (including owner-scoped catalog skills), templates, memory settings and files, instructions, access policy, connectors, channel secrets, mode, Auto, Lean, network and Drive configuration follow the source live. Edit shared settings in the source conversation; attempts to create conflicting selections in a follower explain where to edit. Personal accounts and grants stay personal. Harness, model, effort, engine authentication and sessions remain independent. -An administrator should identify every conversation assigned to the folder, compare direct grants, template-derived grants, organization grants, and memory settings, then either align them or assign separate paths. Separate paths are usually easier to reason about for unrelated projects. +The conversation list shows a linked-folder marker. The folder picker explains inheritance before selection; the conversation editor names the source and provides an Open source conversation button. Slack Settings also identifies the source. One owner runs Drive synchronization; follower sync requests use that same job and state. + +Changing the working folder unlinks the conversation, retaining the last effective setup as its starting point. When the owner leaves, the first remaining conversation becomes the source using its saved setup. The gateway still refuses its private runtime/configuration trees and parent folders that would expose multiple workspaces. Folder assignment remains an administrator operation. + +A shared folder keeps the owner's operating guide stable; a follower on another chat platform receives its actual surface capabilities in each turn's prompt. ## Confirm the effective path diff --git a/documentation/src/content/docs/features/skill-templates.md b/documentation/src/content/docs/features/skill-templates.md index 14272ec8..25a822b0 100644 --- a/documentation/src/content/docs/features/skill-templates.md +++ b/documentation/src/content/docs/features/skill-templates.md @@ -3,6 +3,9 @@ title: Skill templates description: Follow a centrally maintained workflow set while keeping local additions. --- +**Enterprise availability.** Channel skill templates require an effective Enterprise license. Saved templates remain stored after downgrade but do not contribute grants; directly assigned skills remain available. + + A skill template is a reusable selection of explicit skill slugs. Conversations follow its current contents live; a template edit changes subsequent requests in every conversation assigned to it. ## Preview and assign diff --git a/documentation/src/content/docs/features/skills-library-mcp.md b/documentation/src/content/docs/features/skills-library-mcp.md index cfd735f8..6f30529c 100644 --- a/documentation/src/content/docs/features/skills-library-mcp.md +++ b/documentation/src/content/docs/features/skills-library-mcp.md @@ -3,6 +3,9 @@ title: Skills library for external assistants description: Expose the governed catalog to laptops and peer gateways through a scoped MCP endpoint. --- +**Enterprise availability.** The external skill-catalog MCP endpoint and catalog/user access tokens require an effective Enterprise license. Existing tokens cannot access the endpoint after expiry. Ordinary skill use within channels remains available in other tiers. + + The skills library endpoint lets an external MCP client reuse this gateway's catalog without an admin browser session. It is separate from the per-run gateway control server and cannot reach the admin API. ## Connect a client diff --git a/documentation/src/content/docs/features/skills.md b/documentation/src/content/docs/features/skills.md index 2fd84741..16b1d336 100644 --- a/documentation/src/content/docs/features/skills.md +++ b/documentation/src/content/docs/features/skills.md @@ -3,6 +3,9 @@ title: Reusable skills description: Give recurring workflows their own instructions, references, and scripts. --- +**Enterprise availability.** Honoring skill preferred-model switching requires an effective Enterprise license and the conversation switch. Ordinary reusable skills and model metadata remain available in other tiers. + + A skill is a reusable workflow package with a `SKILL.md` entry point and optional references, scripts, and assets. It tells the agent when to use the workflow and how to perform it. Skills do not grant tool permissions or connect provider accounts. ## Add the right skills diff --git a/documentation/src/data/handbook.mjs b/documentation/src/data/handbook.mjs index 6f02e063..9f0ccdca 100644 --- a/documentation/src/data/handbook.mjs +++ b/documentation/src/data/handbook.mjs @@ -730,6 +730,11 @@ export const featureGroups = [ } ]; export const configurationPages = [ + { + "slug": "vps-sizing", + "label": "VPS sizing and hosting", + "description": "Choose server resources, concurrency, storage, and backups for your team." + }, { "slug": "gateway-settings", "label": "Gateway settings", diff --git a/package-lock.json b/package-lock.json index 635e5b52..55e22411 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,30 @@ { "name": "channelgate", - "version": "0.6.3", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "channelgate", - "version": "0.6.3", + "version": "0.6.5", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@composio/core": "0.22.0", - "@modelcontextprotocol/sdk": "^1.0.0", + "@composio/core": "0.28.0", + "@modelcontextprotocol/sdk": "^1.32.2", + "@simplewebauthn/server": "14.0.3", "@slack/bolt": "^5.1.0", "express": "^5.3.0", - "imapflow": "2.9.1", - "mcp-remote": "0.14.3", - "nodemailer": "10.2.1", - "zod": "^4.6.5" + "imapflow": "2.10.2", + "mcp-remote": "0.16.1", + "nodemailer": "10.3.1", + "zod": "^4.6.7" }, "devDependencies": { "acorn": "8.19.0", "acorn-globals": "7.0.1" }, "engines": { - "node": ">=22.13" + "node": ">=24.0.0" } }, "node_modules/@cfworker/json-schema": { @@ -33,29 +34,37 @@ "license": "MIT" }, "node_modules/@composio/client": { - "version": "2.0.0-rc.8", - "resolved": "https://registry.npmjs.org/@composio/client/-/client-2.0.0-rc.8.tgz", - "integrity": "sha512-0C0Flmblx0Y3i6iGUY/MvST4aNXn3xEmPEA4wqZd3acm9x6u61/0i1LXCgBzFTTZEveK8YBFRu8Lpo/SMZwAFQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@composio/client/-/client-2.0.1.tgz", + "integrity": "sha512-1+Azl8W/grinmXVt6KhDKy5UlvTiv7MCMgjqBDxd8TqndjM92/HotxLabnB63A285bvo5Lcu4kbOz413uaPmEQ==", "license": "Apache-2.0", "engines": { "node": ">=22" + }, + "peerDependencies": { + "effect": "*" + }, + "peerDependenciesMeta": { + "effect": { + "optional": true + } } }, "node_modules/@composio/core": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@composio/core/-/core-0.22.0.tgz", - "integrity": "sha512-Inc7/yqhRQTfvU3km/VwSpewwJ9O/5mnC2FjSPDp56MfEdUfx61rwoi6+pancTUhHsHic5qe0lB4C2rHd7iioA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@composio/core/-/core-0.28.0.tgz", + "integrity": "sha512-OF2vC9Bc3uBmTPQKeq43QNWqmsVHHu0AKQJfNaQqM3h8fE8bvG3mUrisJ285mCkVSyY7tAr/dC+ONothQtQRDQ==", "license": "ISC", "dependencies": { - "@composio/client": "2.0.0-rc.8", - "@composio/json-schema-to-zod": "0.3.3", + "@composio/client": "2.0.1", + "@composio/json-schema-to-zod": "0.4.0", "@types/json-schema": "^7.0.15", "is-fs-case-sensitive": "^2.0.0", - "openai": "^7.21.0", + "openai": "^7.39.0", "picocolors": "^1.1.1", "pusher-js": "^8.6.0", "semver": "^7.8.5", - "undici": "^7.29.1", + "undici": "^7.30.1", "zod-to-json-schema": "^3.25.2" }, "engines": { @@ -66,13 +75,14 @@ } }, "node_modules/@composio/json-schema-to-zod": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.3.3.tgz", - "integrity": "sha512-QmgYo4IeK5/34Iqo1jSx6fSnLEyL++07FlW5Y6SfGFOhyb5htnY+vZPjs/tL8puJYhiEU7FV7kXdu5EAQoGrSA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@composio/json-schema-to-zod/-/json-schema-to-zod-0.4.0.tgz", + "integrity": "sha512-BwZXcGpiBFNStECzka7nyF8RogVIzK5kRrDGI5p9P9nlLbQqpCF6H/ILozQE5NoT3HDjbJF26aang+2hDduSNQ==", "license": "ISC", "dependencies": { "@cfworker/json-schema": "^4.1.1", - "dequal": "^2.0.3" + "dequal": "^2.0.3", + "re2js": "^2.8.6" }, "engines": { "node": ">=22.22.3" @@ -81,22 +91,34 @@ "zod": ">=3.25.76 <5" } }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, "node_modules/@hono/node-server": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", - "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.6.tgz", + "integrity": "sha512-p/KH9MnfNcsm3sR6x4lNgm5j+QN4Et8srKredl9DZfXiyZ1UQ3aQ2YVpFLbqktYN5JI+DB+VZFKM2PqE5hLKjQ==", "license": "MIT", "engines": { "node": ">=20" }, "peerDependencies": { - "hono": "^4" + "hono": "^4 || ^5.0.0-0" } }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.32.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", - "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", + "version": "1.32.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz", + "integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", @@ -133,12 +155,266 @@ } } }, + "node_modules/@peculiar/asn1-android": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.11.1.tgz", + "integrity": "sha512-AuUBRDNdWrgtnuJGXARa0Xdtq6OAD8GqezDQz6Qka7kyynMo9fzkcrtpBEHuX984uEB570BrhMZfJfkUDfpQfw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-asym-key": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.11.1.tgz", + "integrity": "sha512-YtXR0FbDtjHuNGvWXD4HFE7th5xYwG67O+ApDH/FZsjj5MOrWHzDvqwZ1vm6uFUJtMAkUDcF6VMPGOJ5gnLyUA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-pkcs8": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.11.1.tgz", + "integrity": "sha512-Bk+oatE62dW0Tzzpfk6vdQxfGGEfvuGdWgmijnKjcv7HqZCJISs85z4zw2uDArrk+R1Oel1wts/BBl7HHrtAcA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "@peculiar/asn1-x509-attr": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.11.1.tgz", + "integrity": "sha512-KVfbXaKng8886/tOPG55t5K1XkTkGbR4amjIJzytXWobFU5nZgw/sKnSpa1hxP17nUSsrOF/7Mt9WUYeyLsi6Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.11.1.tgz", + "integrity": "sha512-7MSNji77fQa/oN3WAfwagMy8EoJFg50sW4hZ1dtr0zU1BSugit0I2DGpIdPhvkNplopQhz5bogxpn1077eaamA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.11.1.tgz", + "integrity": "sha512-2SN2z8qkc2hk/GgTYA8RN54wtsCDZShg9gI2Mq1vLhdj1adcv5fhTbiEmExZUaAMqlizxiEpULQDfHM5iYtn1g==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.11.1", + "@peculiar/asn1-pkcs8": "^2.11.1", + "@peculiar/asn1-rsa": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.11.1.tgz", + "integrity": "sha512-HqsSrnpQghRp9SM9QSmR6fa4z/BNktnKq7928+oq4qF0VAnuHZ4cyePKvcxh9fP4taBCP/WHNItlVNsYaQm2qw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.11.1.tgz", + "integrity": "sha512-OHFkTg8D2zYafILqFE4KPbqX8srpvd3diGDc2h2cduumawc765mOHGcuFMGvLWlL2bbCMWMmMSCtlXptsOuiPA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.11.1", + "@peculiar/asn1-pfx": "^2.11.1", + "@peculiar/asn1-pkcs8": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "@peculiar/asn1-x509-attr": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.11.1.tgz", + "integrity": "sha512-6KwKGjulF489CpidZqkogNgLj6oGzgC4BL2FClM8PuJQ4t5NqT4q4a6A4vrSxBIBhMnL8hGYkLOpbVw0Vq38Tw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.11.1.tgz", + "integrity": "sha512-jaU9OaCcbZUdyXx9hMETZB9xoNmxTGjOGFqd3cOwKFkpKnyKd9WxF3uqj4PvX4cyOMP9bSdHg5YyML2qtx/d/g==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.11.1.tgz", + "integrity": "sha512-SXU4/X6B3FquqQAN6ARI0bFDUV7+L1WlvPEPMKzEMkstd2D7UyM0r93fv241+OjdhqAKYGx5MvV3gzZMAp0APA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.11.1.tgz", + "integrity": "sha512-n7RKFrbTzt8pGBlZ90kmKEn1qsLTTyw7lrvHMK0xsA42kQugPk/GXYpNbEcl7DkkdVsU/one5UX7KQPfxLj8vQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-post-quantum": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.11.1.tgz", + "integrity": "sha512-yD/pxCQ97nzwF7MGAW4kRd1WVLiMPhD39HxOnAr38DfGFwbpVhPnMbq+r6WX0XUrxn4w8ulsTdUafwVl46igLA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-asym-key": "^2.11.1", + "@peculiar/asn1-schema": "^2.11.1", + "@peculiar/asn1-x509": "^2.11.1", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.2.0.tgz", + "integrity": "sha512-adpW58ySPQMOUPv8pTKT3RIyCFqfUFt8X05ukPdtkQToXDVr8dMtxuenik1yx8tNFTbpApWsM/Hi5SzlIx48+Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.11.0", + "@peculiar/asn1-csr": "^2.11.0", + "@peculiar/asn1-ecc": "^2.11.0", + "@peculiar/asn1-pkcs9": "^2.11.0", + "@peculiar/asn1-rsa": "^2.11.0", + "@peculiar/asn1-schema": "^2.11.0", + "@peculiar/asn1-x509": "^2.11.0", + "@peculiar/asn1-x509-post-quantum": "^2.11.0", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@simplewebauthn/server": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.3.tgz", + "integrity": "sha512-D39/0fiDiYmGV+nraY9s63D1Ho2AX/waMLy22/9gUadTUuvbAnstfb4bpbHYbwZuV5ZLDYN1SmzDKuiHzKAA4Q==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-post-quantum": "^2.9.4", + "@peculiar/x509": "^2.1.0", + "reflect-metadata": "^0.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@slack/bolt": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-5.1.0.tgz", @@ -374,13 +650,13 @@ } }, "node_modules/@zone-eu/mailsplit": { - "version": "5.4.29", - "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.29.tgz", - "integrity": "sha512-CCUNycmUjrV5M0wNf23waWAt9eOHczhjLbUQS7k0FNJvR0py9VSqHFt+tSl/D0+XI73VLG0U+PwYtq9R0zsm9Q==", + "version": "5.4.30", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.30.tgz", + "integrity": "sha512-qN0KT6/ziAFvo5TzscaqpurIJaJQEXPPtqBZnpyhiGrACOKLDVMV6Lm57kXt5e/8zZUWoYiSC8oKl/aewtrJDQ==", "license": "(MIT OR EUPL-1.1+)", "dependencies": { "libbase64": "1.4.1", - "libmime": "5.6.3", + "libmime": "5.6.4", "libqp": "3.0.1" }, "engines": { @@ -476,6 +752,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.1.0.tgz", + "integrity": "sha512-HWYi1THkOqguCiO+vwj09ZkQFD2kKEsejYHAYCtX0D80WVdoDnvRc9IukXX/jeb0T9NRWJnRtqwdbHltnFjwXA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.2.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -486,18 +776,18 @@ } }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", + "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" }, @@ -510,9 +800,9 @@ } }, "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { "node": ">=18" @@ -776,9 +1066,9 @@ } }, "node_modules/encoding-japanese": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.4.0.tgz", - "integrity": "sha512-PE0KjlsDy9JSsoU4C2Q8NXb1kC4c+bZRac9ZvHXUE4q1Ht63gScKMdEy9jst9+Fi3M4eDkqzkEhBZvix4blP2A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.5.0.tgz", + "integrity": "sha512-5TKhaMWTUF777NQ16OrAkJFuEd1uOnEBw8ODeXFTc/Y9csZZQ6/jiIS5r4aFuzfowu9gLGrE51zqI2H0dLwlmg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -937,9 +1227,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -1074,9 +1364,9 @@ } }, "node_modules/hono": { - "version": "4.13.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.9.tgz", - "integrity": "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==", + "version": "4.13.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.14.tgz", + "integrity": "sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -1119,16 +1409,16 @@ } }, "node_modules/imapflow": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-2.9.1.tgz", - "integrity": "sha512-hOq2/+T+WnQzRy7D40gYlio25nG+c9bhtrxowokOQM57SwZFqRbqAtxFQcIpx82lhYBPQEEVVjmkYpd9Fplj7g==", + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-2.10.2.tgz", + "integrity": "sha512-KI9tW4KXMz8KuZoSLGwLkXLUH39me38/vYZBL/eyh2CjHVDBFUj1QuqqfzeHJOIj1VwAZ4jL4zoVKppM7cXfrw==", "license": "MIT", "dependencies": { - "@zone-eu/mailsplit": "5.4.29", - "encoding-japanese": "2.4.0", + "@zone-eu/mailsplit": "5.4.30", + "encoding-japanese": "2.5.0", "iconv-lite": "0.7.3", "libbase64": "1.4.1", - "libmime": "5.6.3", + "libmime": "5.6.4", "libqp": "3.0.1", "pino": "10.3.1", "socks": "2.8.10" @@ -1144,9 +1434,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", - "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", "license": "MIT", "engines": { "node": ">= 12" @@ -1304,12 +1594,12 @@ } }, "node_modules/libmime": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.6.3.tgz", - "integrity": "sha512-cuTUW/Jybvh5O4/U9p61gyutHOjDtZyGQYuY99+bfnaFwqcDf2Nb+R6hMHY6DDen2H9wLKQSMpofWUgn2TI5JQ==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.6.4.tgz", + "integrity": "sha512-QWiiwq3ujIUSsCCjFDldDN7FfXuLYSTnEln0oeYfOjgqyVlRV6WQ3UyJ/paguoz8Ynxy1s7k3kh6xomS3bJkCw==", "license": "MIT", "dependencies": { - "encoding-japanese": "2.4.0", + "encoding-japanese": "2.5.0", "iconv-lite": "0.7.3", "libbase64": "1.4.1", "libqp": "3.0.1" @@ -1379,9 +1669,9 @@ } }, "node_modules/mcp-remote": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/mcp-remote/-/mcp-remote-0.14.3.tgz", - "integrity": "sha512-M8eMA+LQIYbVNpntDhbCzFYNxwF9O9KozOuDxOrGTtRXbHRq/m4NuFZ6892hmJcU4Zhv4qPfpxrXyG2FPWNMKg==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/mcp-remote/-/mcp-remote-0.16.1.tgz", + "integrity": "sha512-a09YnLKkGafc3nq619LulpWDgii0CODfr9l5liTcX3kEQkrv+LAzA3EJlGHM9a2SjcSe8JsGJQtmufdLfG42Ig==", "license": "MIT", "dependencies": { "express": "^4.21.2", @@ -1408,9 +1698,9 @@ } }, "node_modules/mcp-remote/node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "version": "1.20.9", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.9.tgz", + "integrity": "sha512-FISIQxV5u91Y9gvuFq3q/tA/qTqLJn3AhRcEvyozyhJTh5OBFFtv827AdHU7Rbyct4r+Ba1/99oFa1Y3yuFsdw==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1421,7 +1711,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.15.1", + "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -1753,9 +2043,9 @@ } }, "node_modules/nodemailer": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.2.1.tgz", - "integrity": "sha512-ZrDNsPFoDMwefNs7sJwrYiEJ/CG4ClJWDZv05iNJsEBWa8GRDry1QJHTJxsuTAY9eDDKjX4k+q3SxslXJOao7w==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.3.1.tgz", + "integrity": "sha512-uF0AFsJC07Y1vdW7p580DKAnh7rAO+4UPSSB3mDCPNDufoiaOebV6JmrzpxB14OjEEEiBsNdYoM2Y+fN8M7BBA==", "license": "MIT-0", "engines": { "node": ">=20.0.0" @@ -1831,9 +2121,9 @@ } }, "node_modules/openai": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-7.23.0.tgz", - "integrity": "sha512-0ecOXnFSMNWZq6cUBcTV6Lf93y+fm8BH/+QzFvpoP1UGNOE5pnytRa6HAPGCw+3IS9SIAW7rc1M8yyzN1PiBAQ==", + "version": "7.52.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.52.0.tgz", + "integrity": "sha512-aY0e5WGnTfAae81ygo+cbRaYwSQNowoyyxvAKNC0vu8orz3bl4cbz1il3Ce6I2rNea5et/FGDxj82OO9iB4+nA==", "license": "Apache-2.0", "engines": { "node": ">=22.0.0" @@ -1842,6 +2132,7 @@ "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", + "google-auth-library": ">=9.15.1 <12", "undici": ">=5 <9", "ws": "^8.21.0", "zod": "^3.25 || ^4.0" @@ -1856,6 +2147,9 @@ "@smithy/signature-v4": { "optional": true }, + "google-auth-library": { + "optional": true + }, "undici": { "optional": true }, @@ -2045,10 +2339,28 @@ "tweetnacl": "^1.0.3" } }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -2091,6 +2403,15 @@ "node": ">= 0.10" } }, + "node_modules/re2js": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.9.1.tgz", + "integrity": "sha512-JIT3bEEUZTOR2YS1walNEIfH4cF/cQeZcBUFq7ldqO6lR6g0X5CBq/S8rsa9omzZcsNf8qc36jCkWpKKtncrZg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -2100,6 +2421,12 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2421,6 +2748,12 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -2430,6 +2763,24 @@ "node": ">=0.6.x" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -2468,9 +2819,9 @@ } }, "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -2546,9 +2897,9 @@ } }, "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 9a643ca1..6195d52e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "channelgate", - "version": "0.6.3", + "version": "0.6.5", "private": true, "license": "SEE LICENSE IN LICENSE.md", "author": "Tiberiu Socaci (MAKEITFUTURE S.R.L.)", @@ -26,6 +26,7 @@ "test:coverage": "node scripts/run-tests.mjs --coverage", "test:security-coverage": "node scripts/security-coverage.mjs", "runtime:storage": "node scripts/runtime-storage.mjs", + "admin:recover": "node scripts/admin-recover.mjs", "test:live-container": "CG_LIVE_CONTAINER=1 node --test test/container-durability.live.test.js test/ssh-installer-access.test.js test/vscode-shared-server.live.test.js", "check:static": "node scripts/static-check.mjs", "check:dco": "node scripts/check-dco.mjs", @@ -50,23 +51,24 @@ "vpn": "node scripts/channel-vpn.mjs" }, "engines": { - "node": ">=22.13" + "node": ">=24.0.0" }, "dependencies": { - "@composio/core": "0.22.0", - "@modelcontextprotocol/sdk": "^1.0.0", + "@composio/core": "0.28.0", + "@modelcontextprotocol/sdk": "^1.32.2", + "@simplewebauthn/server": "14.0.3", "@slack/bolt": "^5.1.0", "express": "^5.3.0", - "imapflow": "2.9.1", - "mcp-remote": "0.14.3", - "nodemailer": "10.2.1", - "zod": "^4.6.5" + "imapflow": "2.10.2", + "mcp-remote": "0.16.1", + "nodemailer": "10.3.1", + "zod": "^4.6.7" }, "devDependencies": { "acorn": "8.19.0", "acorn-globals": "7.0.1" }, "overrides": { - "qs": "6.16.0" + "qs": "6.16.1" } } diff --git a/public/admin-access.js b/public/admin-access.js new file mode 100644 index 00000000..8733fc67 --- /dev/null +++ b/public/admin-access.js @@ -0,0 +1,379 @@ +import { api } from "./admin-api.js"; + +const passkeys = () => window.ChannelGatePasskeys; + +// Passwords and codes stay only in this temporary form; never browser storage. +async function passwordSlackReauth(me) { + const previousFocus = document.activeElement; + const modal = document.createElement("div"); + modal.className = "modal"; + modal.innerHTML = ``; + document.body.append(modal); + const at = (name) => modal.querySelector(`[data-auth="${name}"]`); + at("passkey").hidden = !me.passkeys?.length; + let requestId = ""; + let busy = false; + return new Promise((resolve, reject) => { + function finish(value, error) { + at("password").value = ""; + at("code").value = ""; + document.removeEventListener("keydown", onKey); + modal.remove(); + previousFocus?.focus(); + if (error) reject(error); else resolve(value); + } + function onKey(event) { + if (event.key === "Escape" && !busy) { event.preventDefault(); finish(false, new Error("Identity verification cancelled.")); } + if (event.key === "Tab") { + const controls = [...modal.querySelectorAll("input, button")].filter((item) => !item.disabled && !item.closest("[hidden]")); + const first = controls[0]; + const last = controls.at(-1); + if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } + } + } + document.addEventListener("keydown", onKey); + at("cancel").addEventListener("click", () => finish(false, new Error("Identity verification cancelled."))); + at("passkey").addEventListener("click", () => finish(false)); + modal.querySelector("form").addEventListener("submit", async (event) => { + event.preventDefault(); + if (busy) return; + busy = true; + for (const button of modal.querySelectorAll("button")) button.disabled = true; + at("error").textContent = ""; + try { + if (!requestId) { + let result; + try { + result = await api("/api/auth/password-slack/options", { method: "POST", body: JSON.stringify({ password: at("password").value }) }); + } finally { at("password").value = ""; } + requestId = result.requestId; + at("password").required = false; + at("password-label").hidden = true; + at("code-label").hidden = false; + at("code").required = true; + at("status").textContent = "Check your private Slack messages. The code expires in 5 minutes."; + at("submit").textContent = "Verify code"; + at("code").focus(); + } else { + try { + await api("/api/auth/password-slack/verify", { method: "POST", body: JSON.stringify({ requestId, code: at("code").value.trim() }) }); + } finally { at("code").value = ""; } + finish(true); + } + } catch (error) { at("error").textContent = error.message; } + finally { + busy = false; + for (const button of modal.querySelectorAll("button")) button.disabled = false; + } + }); + at("password").focus(); + }); +} + +// Called from a user action (settings, secret reveal, account/key management). The server is +// authoritative about freshness; never cache a password or an assertion in browser storage. +export async function freshAdminAuth({ force = false } = {}) { + const me = await api("/api/auth/me", { cache: "no-store" }); + if (me.bootstrap) throw new Error("Create your first passkey under Settings → Admin access before continuing."); + if (!force && me.fresh) return true; + if (me.passwordSlackEnabled && /^[UW][A-Z0-9]{8,}$/.test(me.account?.userId || "") && await passwordSlackReauth(me)) return true; + try { + const { options } = await api("/api/auth/reauth/options", { method: "POST", body: "{}" }); + const response = await passkeys().get(options); + await api("/api/auth/reauth/verify", { method: "POST", body: JSON.stringify({ response }) }); + return true; + } catch (error) { + throw new Error(passkeys().errorMessage(error)); + } +} + +const dateLabel = (value) => { + if (!value) return "Never"; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? "Unknown" : date.toLocaleString(); +}; + +function accessRow(title, detail, actionLabel, action, disabled = false) { + const item = document.createElement("li"); + const text = document.createElement("div"); + const heading = document.createElement("strong"); + heading.textContent = title; + const secondary = document.createElement("small"); + secondary.textContent = detail; + text.append(heading, secondary); + item.append(text); + if (action) { + const button = document.createElement("button"); + button.type = "button"; + button.className = "ghost"; + button.textContent = actionLabel; + button.disabled = disabled; + button.dataset.unavailable = String(disabled); + button.addEventListener("click", action); + item.append(button); + } + return item; +} + +export async function mountAdminAccess(container, { confirm = async () => false, onLinked = null } = {}) { + if (!container) return; + const generation = Symbol("admin-access-mount"); + container._adminAccessMount = generation; + const active = () => container._adminAccessMount === generation; + container.innerHTML = ` +

Your dashboard access

+

Dashboard access belongs to existing gateway admin users. Manage other admins from Users. Changes here save immediately.

+

Loading your identity…

+ + +
+ +

Admins can sign in without a passkey using the shared dashboard password and a private Slack code. Requires a Slack admin account and the Slack app permission users:read.email. Disabling this option signs out password and Slack code sessions.

+ + +
+
+ + +
+

Register a spare passkey so you can still sign in if you lose your primary device. Your last passkey cannot be removed here.

+
    + +

    `; + const at = (name) => container.querySelector(`[data-access="${name}"]`); + let me; + let busy = false; + let ready = false; + let linkRequestId = ""; + function syncControls() { + if (!active()) return; + for (const control of container.querySelectorAll("button, select, input[type=checkbox]")) control.disabled = !ready || busy || control.dataset.unavailable === "true"; + } + syncControls(); + function message(text, error = false) { + if (!active()) return; + at("message").textContent = text; + at("message").classList.toggle("error", error); + } + async function run(action) { + if (!ready || busy || !active()) return; + busy = true; + message(""); + syncControls(); + try { await action(); } + catch (error) { message(passkeys().errorMessage(error), true); } + finally { + busy = false; + syncControls(); + } + } + async function refresh() { + const accountState = await api("/api/auth/me", { cache: "no-store" }); + if (!active()) return; + me = accountState; + at("identity").textContent = me.account.userId + ? `Signed in as ${me.account.displayName || me.account.username} · ${me.account.userId}` + : "Your current passkeys are not yet linked to a gateway user."; + at("link-panel").hidden = !(me.canLink || me.linkRequired); + if (me.canLink || me.linkRequired) { + const selectedUser = at("link-user").value; + const { users } = await api("/api/auth/link/users", { cache: "no-store" }); + if (!active()) return; + at("link-user").replaceChildren(...users.map((user) => { + const option = document.createElement("option"); + option.value = user.id; + option.textContent = `${user.name || user.id} (${user.id})`; + return option; + })); + if (users.some((user) => user.id === selectedUser)) at("link-user").value = selectedUser; + at("link-send").dataset.unavailable = String(!users.length); + if (!users.length) at("link-status").textContent = "No existing admin user is available to link. Configure your chat connection and admin users first."; + } + at("bootstrap").hidden = !me.bootstrap; + at("password-slack-label").textContent = me.bootstrap ? "Existing setup password (required to enable sign-in)" : "Dashboard password (leave blank to keep the current password)"; + at("password-slack-enabled").checked = Boolean(me.passwordSlackEnabled); + at("password-slack-save").dataset.unavailable = String(!me.account.userId || me.linkRequired); + at("password-slack-enabled").dataset.unavailable = String(!me.account.userId || me.linkRequired); + at("register").textContent = me.passkeys.length ? "Add a passkey" : "Create your first passkey"; + at("register").dataset.unavailable = String(Boolean(me.linkRequired)); + at("keys").replaceChildren(...me.passkeys.map((key) => accessRow( + key.name || "Passkey", `Added ${dateLabel(key.createdAt)} · Last used ${dateLabel(key.lastUsedAt)}`, + "Remove", () => run(async () => { + if (!await confirm({ title: "Remove passkey?", body: `Remove “${key.name || "Passkey"}” from your identity?`, confirmLabel: "Remove", danger: true })) return; + await freshAdminAuth(); + await api(`/api/auth/passkeys/${encodeURIComponent(key.id)}`, { method: "DELETE" }); + await refresh(); + message("Passkey removed."); + }), me.passkeys.length <= 1 || me.linkRequired, + ))); + if (!me.passkeys.length) at("keys").append(accessRow("No passkeys yet", me.linkRequired ? "Verify your gateway user above before creating the first passkey." : "Create your first passkey above to finish setup.")); + at("reauth").dataset.unavailable = String(Boolean(me.bootstrap)); + syncControls(); + } + at("link-send-form").addEventListener("submit", (event) => { + event.preventDefault(); + void run(async () => { + if (!me.bootstrap) await freshAdminAuth(); + const result = await api("/api/auth/link/options", { method: "POST", body: JSON.stringify({ userId: at("link-user").value }) }); + linkRequestId = result.requestId; + at("link-code").value = ""; + at("link-verify-form").hidden = false; + at("link-status").textContent = "Check the selected user's private chat messages for an 8-digit code. It expires in 5 minutes."; + at("link-code").focus(); + }); + }); + at("link-verify-form").addEventListener("submit", (event) => { + event.preventDefault(); + void run(async () => { + await api("/api/auth/link/verify", { method: "POST", body: JSON.stringify({ requestId: linkRequestId, code: at("link-code").value.trim() }) }); + linkRequestId = ""; + at("link-code").value = ""; + await refresh(); + message("Your passkeys are now linked to your gateway admin user."); + if (onLinked) await onLinked(); + }); + }); + at("link-user").addEventListener("change", () => { + linkRequestId = ""; + at("link-code").value = ""; + at("link-verify-form").hidden = true; + at("link-status").textContent = "Send a new verification code to the selected user."; + }); + async function savePasswordSlack() { + const enabled = at("password-slack-enabled").checked; + const password = at("password-slack-password").value; + at("password-slack-password").value = ""; + try { + if (!me.bootstrap) await freshAdminAuth(); + const result = await api("/api/auth/password-slack/settings", { method: "PUT", body: JSON.stringify({ enabled, ...(password ? { password } : {}) }) }); + if (result.signedOut) { location.replace("/login.html"); return; } + await refresh(); + message(enabled ? "Password and Slack code sign-in enabled." : "Password and Slack code sign-in disabled."); + if (enabled && !me.bootstrap && !me.passkeys.length) location.replace("/settings#set-admin-access"); + } catch (error) { + at("password-slack-enabled").checked = Boolean(me.passwordSlackEnabled); + throw error; + } + } + at("password-slack-form").addEventListener("submit", (event) => { + event.preventDefault(); + void run(savePasswordSlack); + }); + at("password-slack-enabled").addEventListener("change", () => { void run(savePasswordSlack); }); + at("register-form").addEventListener("submit", (event) => { + event.preventDefault(); + void run(async () => { + if (!me.bootstrap) await freshAdminAuth(); + const { options } = await api("/api/auth/register/options", { method: "POST", body: "{}" }); + const response = await passkeys().create(options); + await api("/api/auth/register/verify", { method: "POST", body: JSON.stringify({ response, name: at("key-name").value.trim() }) }); + at("key-name").value = ""; + await refresh(); + message("Passkey added. You can use it the next time you sign in."); + }); + }); + at("reauth").addEventListener("click", () => run(async () => { + await freshAdminAuth({ force: true }); + await refresh(); + message("Identity verified. Sensitive actions are available for a short time."); + })); + try { await refresh(); if (active()) ready = true; } + catch (error) { message(passkeys().errorMessage(error), true); } + finally { syncControls(); } +} + +export async function mountUserDashboardAccess(container, userId, { confirm = async () => false } = {}) { + if (!container) return; + container.innerHTML = ` +

    Dashboard access

    +

    Loading passkey status…

    +
      +
      + + +
      + +

      `; + const at = (name) => container.querySelector(`[data-dashboard="${name}"]`); + let busy = false; + const message = (text, error = false) => { + at("message").textContent = text; + at("message").classList.toggle("error", error); + }; + const refresh = async () => { + const [state, actor] = await Promise.all([ + api(`/api/auth/users/${encodeURIComponent(userId)}`, { cache: "no-store" }), + api("/api/auth/me", { cache: "no-store" }), + ]); + at("status").textContent = !state.isAdmin + ? "Only gateway admin users can sign in to the dashboard. Save the Admin role above before inviting this user." + : state.passkeyCount ? `${state.passkeyCount} registered passkey${state.passkeyCount === 1 ? "" : "s"}. Dashboard access follows this user's Admin role.` + : "This admin has not registered a passkey yet. Send a private setup link to invite them to the dashboard."; + const canInvite = state.canInvite && Boolean(actor.account.userId) && !actor.bootstrap; + at("invite").hidden = !state.isAdmin || state.passkeyCount > 0; + at("invite").disabled = !canInvite; + at("invite").dataset.unavailable = String(!canInvite); + at("invite").textContent = state.linked ? "Create new setup link" : "Invite to dashboard"; + at("revoke").hidden = !state.isAdmin || !state.linked; + at("keys").replaceChildren(...state.passkeys.map((key) => accessRow(key.name || "Passkey", `Added ${dateLabel(key.createdAt)} · Last used ${dateLabel(key.lastUsedAt)}`))); + if (state.isAdmin && !actor.account.userId) at("status").textContent += " Link your own admin identity under Settings → Admin access before inviting users."; + }; + const run = async (action) => { + if (busy) return; + busy = true; + message(""); + for (const button of container.querySelectorAll("button")) button.disabled = true; + try { await action(); } + catch (error) { message(passkeys().errorMessage(error), true); } + finally { + busy = false; + for (const button of container.querySelectorAll("button")) button.disabled = button.dataset.unavailable === "true"; + } + }; + at("invite").addEventListener("click", () => run(async () => { + at("invite-url").value = ""; + at("invite-panel").hidden = true; + await freshAdminAuth(); + const { setupUrl } = await api(`/api/auth/users/${encodeURIComponent(userId)}/invite`, { method: "POST", body: "{}" }); + at("invite-url").value = setupUrl; + at("invite-panel").hidden = false; + message("Setup link created. Share it privately with this user."); + })); + at("revoke").addEventListener("click", () => run(async () => { + if (!await confirm({ title: "Sign out dashboard sessions?", body: "This user will need a passkey to sign in again. Registered passkeys are kept.", confirmLabel: "Sign out", danger: true })) return; + await freshAdminAuth(); + await api(`/api/auth/users/${encodeURIComponent(userId)}/revoke-sessions`, { method: "POST", body: "{}" }); + message("Dashboard sessions signed out."); + })); + at("copy").addEventListener("click", () => run(async () => { + try { await navigator.clipboard.writeText(at("invite-url").value); message("Setup link copied. Share it privately with this user."); } + catch { at("invite-url").focus(); at("invite-url").select(); message("Select and copy the setup link above."); } + })); + at("dismiss").addEventListener("click", () => { at("invite-url").value = ""; at("invite-panel").hidden = true; }); + try { await refresh(); } + catch (error) { message(passkeys().errorMessage(error), true); } +} diff --git a/public/admin-api.js b/public/admin-api.js index 6f101ab7..8ccea055 100644 --- a/public/admin-api.js +++ b/public/admin-api.js @@ -7,6 +7,9 @@ export async function api(url, options = {}) { }); const body = await res.json().catch(() => ({})); if (!res.ok) { + // A temporary operator's shell may already be open when the first chat admin appears. + // Re-enter through the identity gate rather than leave a stale dashboard over the remedy. + if (body.code === "identity_link_required" && typeof window !== "undefined" && typeof window.location?.reload === "function") window.location.reload(); // Keep the status and the server's code on the error: a caller that can offer a real remedy // (the Host/Origin guard is the one refusal the operator can act on) must be able to tell // this failure apart from a generic one, and prose matching would break on a reworded message. diff --git a/public/admin-schedules.js b/public/admin-schedules.js index d8dfdf6a..e852d35b 100644 --- a/public/admin-schedules.js +++ b/public/admin-schedules.js @@ -29,7 +29,7 @@ export function automationSummaryMarkup(summary) { detail("No result", summary.noOp || 0), detail("Failed / blocked", `${summary.failed || 0} / ${summary.blocked || 0}`), detail("Unreported / interrupted", `${summary.unreported || 0} / ${summary.interrupted || 0}`), detail("Engine completed", summary.engineCompleted || 0), detail("Delivery failures", summary.deliveryFailed || 0), detail("Skipped", summary.skipped || 0), - detail("Last run requested", date(summary.lastStartedAt)), detail("Recorded cost", summary.totalCostUsd == null ? "—" : `$${Number(summary.totalCostUsd).toFixed(4)}`), + detail("Last run requested", date(summary.lastStartedAt)), detail(summary.costHistoryDays ? `Recorded cost (last ${summary.costHistoryDays} days)` : "Recorded cost", summary.totalCostUsd == null ? "—" : `$${Number(summary.totalCostUsd).toFixed(4)}`), ].join("")}

      Task outcomes and connection checks are reported by the agent. Engine completion and delivery are tracked separately.

      `; } export function automationRunMarkup(run) { diff --git a/public/admin-secrets.js b/public/admin-secrets.js index d5f8f86d..76ba8e23 100644 --- a/public/admin-secrets.js +++ b/public/admin-secrets.js @@ -20,6 +20,7 @@ export function mountSecretEditor({ savedText = (name) => `Saved ${name}. It reaches the next run.`, namePlaceholder = "GH_TOKEN", disabled = false, + allowRemoval = false, // Slack user id → display name (the admin app's user directory); "" when unknown. userName = () => "", }) { @@ -84,7 +85,7 @@ export function mountSecretEditor({ cell(entry.setAt ? new Date(entry.setAt).toISOString().slice(0, 10) : "—", { className: "state" }); const actions = row.insertCell(); actions.className = "vars-actions"; - if (!disabled) { + if (!disabled || allowRemoval) { const remove = document.createElement("button"); remove.type = "button"; remove.className = "ghost secret-remove"; @@ -127,6 +128,7 @@ export function mountSecretEditor({ nameInput.addEventListener("blur", () => { nameInput.value = nameInput.value.trim().toUpperCase(); }); saveButton.addEventListener("click", async () => { + if (disabled) return; const name = nameInput.value.trim().toUpperCase(); nameInput.value = name; // what gets sent is what the admin can see const value = valueInput.value; @@ -154,7 +156,7 @@ export function mountSecretEditor({ } catch (e) { hint.textContent = e.message || "Couldn't save that variable."; } finally { - saveButton.disabled = false; + saveButton.disabled = disabled; } }); diff --git a/public/admin-skills.js b/public/admin-skills.js index cc984d7f..7c9aa2e7 100644 --- a/public/admin-skills.js +++ b/public/admin-skills.js @@ -1,4 +1,4 @@ -// The Skills view of the admin UI: the local skill catalog (browse, read, author, pin/rollback, +// The Skills view of the admin UI: the gateway skill catalog (browse, read, author, pin/rollback, // remove), the review queue (staged source revisions + proposals), sources (GitHub / gateway, // sync, settings), templates (edit, preview and apply to a conversation) and usage. Talks to // src/web/routes/skills.js. No framework — one delegated click handler per panel. @@ -26,7 +26,8 @@ const state = { sourceSkills: [], sourceQuery: "", sourceQueryDraft: "", - enabled: "1", + enabled: "all", + missingSkillMd: "all", discoverable: "all", mandatory: "all", assigned: "all", @@ -51,7 +52,8 @@ const state = { const body = () => document.getElementById("skills-body"); const fmtWhen = (ts) => (ts ? String(ts).replace("T", " ").slice(0, 16) : "—"); -const ownerLabel = (s) => (s.ownerKind === "git" ? `git source #${s.sourceId}` : s.ownerKind); +const ownerKindLabel = (kind) => kind === "local" ? "Gateway-owned" : kind; +const ownerLabel = (s) => (s.ownerKind === "git" ? `git source #${s.sourceId}` : ownerKindLabel(s.ownerKind)); function setMessage(msg, isError = false) { state.message = isError ? "" : msg; @@ -78,6 +80,7 @@ async function refreshAll() { state.source ? `source=${encodeURIComponent(state.source)}` : "", state.category ? `category=${encodeURIComponent(state.category)}` : "", state.enabled ? `enabled=${state.enabled}` : "", + `missingSkillMd=${state.missingSkillMd}`, state.discoverable ? `discoverable=${state.discoverable}` : "", state.mandatory ? `mandatory=${state.mandatory}` : "", state.assigned ? `assigned=${state.assigned}` : "", @@ -93,6 +96,7 @@ async function refreshAll() { ...catalog, skills: filterSkillCatalog(catalog.skills, { enabled: state.enabled, + missingSkillMd: state.missingSkillMd, discoverable: state.discoverable, mandatory: state.mandatory, assigned: state.assigned, @@ -100,7 +104,7 @@ async function refreshAll() { category: state.category, }, { overview, profiles: profiles.profiles }), }; - state.catalogAll = catalogAll; + state.catalogAll = { ...catalogAll, skills: catalogAll.skills.filter((skill) => !skill.diagnosticOnly) }; state.profiles = profiles.profiles; if (state.sourceSelected) { const result = await api(`/api/skills/catalog?source=${encodeURIComponent(state.sourceSelected)}&deleted=1`); @@ -247,24 +251,28 @@ function renderCatalog() { ["1", yes], ["0", no], ].map(([value, label]) => ``).join(""); - const rows = skills.map((s) => ` + const rows = skills.map((s) => s.diagnosticOnly ? ` + ${esc(s.slug)} Missing SKILL.md + ${esc(s.description)}—Host folder— + Unavailable — fix the folder or remove its grant in Users.0` : ` - ${esc(s.slug)}${pluginBadge(s)}${s.visibility === "personal" ? ' personal' : ""}${s.excluded ? ' excluded' : s.deleted ? ' removed' : ""}${s.channelScope ? ` ${esc(channelLabel(s.channelScope))}` : ""}${s.pinnedRevisionId ? ' pinned' : ""}${s.stagedCount ? ` ${s.stagedCount} staged` : ""}${s.currentRevisionId == null && !s.deleted ? ' not active' : ""} + ${esc(s.slug)}${pluginBadge(s)}${s.model ? ` ${esc(s.model)}` : ""}${s.visibility === "personal" ? ' personal' : ""}${s.excluded ? ' excluded' : s.deleted ? ' removed' : ""}${s.channelScope ? ` ${esc(channelLabel(s.channelScope))}` : ""}${s.pinnedRevisionId ? `${s.pinReason === "publication" ? "publication recovery" : "pinned"}` : ""}${s.stagedCount ? ` ${s.stagedCount} staged` : ""}${s.currentRevisionId == null && !s.deleted ? ' not active' : ""} ${esc(s.description)} ${esc(s.category || "—")} ${esc(ownerLabel(s))} ${esc(s.version || "—")} - + ${s.usage30d?.total || 0} `).join(""); return `
      ${searchBox("skills-q", { label: "Search the skill catalog", placeholder: "Search slug, name, description, tags…" })} - + + @@ -283,7 +291,7 @@ function renderCatalog() { function renderNewSkillForm() { return `
      -

      New local skill

      +

      New gateway-owned skill

      @@ -325,12 +333,12 @@ function renderDetail() {

      ${esc(s.slug)} ${esc(s.name !== s.slug ? s.name : "")}${pluginBadge(s)}

      ${esc(s.owner)}${s.excluded ? 'excluded' : s.deleted ? 'removed' : ""}

      ${esc(s.description)}

      ${pluginSummary(s)} -

      Category: ${esc(s.category || "—")} · Version: ${esc(s.version || "—")} · Tags: ${esc((s.tags || []).join(", ") || "—")} · Requires: ${esc((s.requires || []).join(", ") || "—")}${s.createdBy ? ` · Author: ${esc(s.createdBy)}` : ""}

      +

      Category: ${esc(s.category || "—")} · Version: ${esc(s.version || "—")} · Tags: ${esc((s.tags || []).join(", ") || "—")} · Requires: ${esc((s.requires || []).join(", ") || "—")} · Preferred model: ${esc(s.model || "—")}${s.createdBy ? ` · Author: ${esc(s.createdBy)}` : ""}

      ${esc(usage)}

      Files (effective revision)
      ${files || 'no approved revision yet'}
      ${state.fileView ? `
      ${esc(state.fileView.content)}
      ` : ""} -
      Revisions${s.pinnedRevisionId ? ' — pinned ' : ""}
      +
      Revisions${s.pinnedRevisionId ? ` — ${s.pinReason === "publication" ? "publication recovery (clears after successful publication)" : "pinned"} ` : ""}
      ${revs}
      Grant to @@ -342,7 +350,7 @@ function renderDetail() { ${s.ownerKind === "local" ? `` : ""} - ${(state.overview?.orgSkills || []).some((x) => x.toLowerCase() === s.slug.toLowerCase()) ? `` : ``} + ${(state.overview?.orgSkills || []).some((x) => x.toLowerCase() === s.slug.toLowerCase()) ? `` : ``} ${state.overview?.settings?.publish && s.ownerKind === "local" ? `` : ""} ${s.deleted ? `` : ``} @@ -357,7 +365,7 @@ function renderReview() { ${esc(r.slug)}${pluginBadge(r)} ${esc(r.skillName || "")}${pluginSummary(r)} #${r.revisionNo}${r.version ? ` v${esc(r.version)}` : ""} · ${r.fileCount} file(s) - ${esc(r.ownerKind === "git" ? `git source #${r.sourceId}` : r.ownerKind)} · ${esc(r.sourceRef.slice(0, 12))} + ${esc(ownerLabel(r))} · ${esc(r.sourceRef.slice(0, 12))} ${fmtWhen(r.createdAt)} `).join(""); @@ -374,7 +382,7 @@ function renderReview() { ${stagedRows || ''}
      Skill or pluginRevisionFromReceived
      Nothing staged.
      ${state.fileView?.kind === "revision" ? `

      Revision #${state.fileView.revisionNo} files

      ${state.fileView.files.map((f) => `

      ${esc(f.path)} ${f.size} B

      ${f.content != null ? `
      ${esc(f.content)}
      ` : ""}`).join("")}
      ` : ""}

      Proposals

      -

      Changes members proposed from chat. Approving a change publishes a revision (pinned as a local override when the skill comes from a source); approving a promotion grants the skill organization-wide.

      +

      Changes members proposed from chat. Approving a change publishes a revision (pinned as a gateway override when the skill comes from a source); approving a promotion grants the skill organization-wide.

      ${proposalRows || ''}
      ProposalNote / filesBy
      No pending proposals.
      ${state.fileView?.kind === "proposal" ? `

      Proposal #${state.fileView.id} files

      ${state.fileView.files.map((f) => `

      ${esc(f.path)}

      ${esc(f.encoding === "base64" ? "(binary)" : f.content)}
      `).join("")}
      ` : ""}`; } @@ -401,7 +409,7 @@ function renderSourceSkills(source) { const all = state.sourceSkills; const q = state.sourceQuery.trim().toLowerCase(); const skills = all.filter((skill) => !q || `${skill.slug} ${skill.name} ${skill.description}`.toLowerCase().includes(q)); - const toggle = (skill, key, label) => ``; + const toggle = (skill, key, label) => ``; return `
      ${esc(source.kind)}

      ${esc(sourceName(source))}

      ${esc(source.url)}

      ${all.length} skills${all.filter((s) => s.enabled).length} enabled${all.filter((s) => s.discoverable).length} discoverable${all.filter((s) => s.mandatory).length} mandatory
      @@ -486,7 +494,7 @@ function renderMcp() {

      MCP access

      Connect external assistants or another ChannelGate to this catalog.

      MCP endpoint & access tokens

      -

      Any MCP client — Claude Code on a laptop, Codex, another gateway — can use this catalog at ${settings.mcpUrl ? `${esc(settings.mcpUrl)}` : "<public URL>/mcp/skills"} with a bearer token minted here. Scopes: read (search/read), propose (suggest changes), manage (create/update local skills), sync (export, for a peer gateway). The value is shown once.

      +

      Any MCP client — Claude Code on a laptop, Codex, another gateway — can use this catalog at ${settings.mcpUrl ? `${esc(settings.mcpUrl)}` : "<public URL>/mcp/skills"} with a bearer token. Catalog tokens are minted here for other systems and peer gateways: the library (organization and channel skills), never a personal skill. Scopes: read (search/read), propose (suggest changes), manage (create/update library skills), sync (export, for a peer gateway). User tokens are minted by each person from chat (create_my_skill_token) and act as that person — their organization, channel and personal skills, resolved live; they are listed here and an admin may revoke them. Values are shown once.

      @@ -496,11 +504,12 @@ function renderMcp() {
      ${state.newToken ? `

      Token ${esc(state.newToken.record.name)} — copy it now, it will not be shown again:

      ${esc(state.newToken.token)}
      claude mcp add --transport http channelgate-skills ${esc(settings.mcpUrl || "/mcp/skills")} --header "Authorization: Bearer ${esc(state.newToken.token)}"
      ` : ""} - ${(state.overview?.tokens || []).map((t) => ``).join("") || ''}
      TokenScopesCreatedLast used
      ${esc(t.name)} ${esc(t.prefix)}…${t.revoked ? ' revoked' : ""}${esc(t.scopes.join(", "))}${fmtWhen(t.createdAt)}${fmtWhen(t.lastUsedAt)}${t.revoked ? `` : ``}
      No tokens yet.
      + ${(state.overview?.tokens || []).map((t) => ``).join("") || ''}
      TokenKindScopesCreatedLast used
      ${esc(t.name)} ${esc(t.prefix)}…${t.revoked ? ' revoked' : ""}${t.kind === "user" ? `user · ${esc(t.ownerId || "")}` : "catalog"}${esc((t.scopes || []).join(", "))}${fmtWhen(t.createdAt)}${fmtWhen(t.lastUsedAt)}${t.revoked ? `` : ``}
      No tokens yet.
      `; } function renderTemplates() { + if (!state.overview?.features?.skillTemplates) return `

      Skill templates · Enterprise

      Channel skill templates require Enterprise. Saved templates and assignments are retained and inactive on Free. You can still grant skills directly to a conversation.

      ${(state.overview?.templates || []).map((template) => `

      ${esc(template.name)} · saved${template.builtin ? " · built-in" : ` `}

      `).join("")}
      `; const templates = state.overview?.templates || []; const e = state.editTemplate; const form = e ? ` @@ -892,12 +901,13 @@ function wire() { } // Not "skills-q": a search box commits on Enter or its Search button, never on blur. Picking // a filter here does apply whatever is currently typed — that click is a deliberate ask too. - if (["skills-owner", "skills-source", "skills-category", "skills-enabled", "skills-discoverable", "skills-mandatory", "skills-assigned"].includes(el.id)) { + if (["skills-owner", "skills-source", "skills-category", "skills-enabled", "skills-discoverable", "skills-mandatory", "skills-assigned", "skills-missing-manifest"].includes(el.id)) { state.query = state.queryDraft = val("skills-q"); state.owner = val("skills-owner"); state.source = val("skills-source"); state.category = val("skills-category"); state.enabled = val("skills-enabled"); + state.missingSkillMd = val("skills-missing-manifest"); state.discoverable = val("skills-discoverable"); state.mandatory = val("skills-mandatory"); state.assigned = val("skills-assigned"); diff --git a/public/admin-view.js b/public/admin-view.js index 59d910a6..4548058a 100644 --- a/public/admin-view.js +++ b/public/admin-view.js @@ -1,4 +1,5 @@ import { api } from "./admin-api.js"; +import { freshAdminAuth } from "./admin-access.js"; export const escapeHtml = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); @@ -94,13 +95,24 @@ async function askRevealPassword() { export function revealSecret(scope, field, id = "") { return async () => { - const password = await askRevealPassword(); - if (!password) return ""; + const identity = await api("/api/auth/me"); + let password = ""; + if (identity.bootstrap) { + password = await askRevealPassword(); + if (!password) return ""; + } else { + await freshAdminAuth(); + } try { const { value } = await api("/api/secrets/reveal", { method: "POST", body: JSON.stringify({ scope, field, id, password }) }); return value || ""; } catch (error) { if (/password/i.test(error.message)) { revealPassword = ""; revealPasswordAt = 0; } + if (error.code === "reauth_required") { + await freshAdminAuth({ force: true }); + const { value } = await api("/api/secrets/reveal", { method: "POST", body: JSON.stringify({ scope, field, id }) }); + return value || ""; + } throw error; } }; diff --git a/public/app.js b/public/app.js index 0656f2e3..5ae06add 100644 --- a/public/app.js +++ b/public/app.js @@ -18,7 +18,8 @@ import { import { activeSectionFor, filterSettings } from "./admin-settings-search.js"; import { api } from "./admin-api.js"; import { setSystemHealthActive } from "./admin-system-health.js"; -import { attachReveal, confirmDialog, escapeHtml, infoDialog, openDialog, paintReveal, passwordDialog, revealSecret, tokenValue } from "./admin-view.js"; +import { attachReveal, confirmDialog, escapeHtml, infoDialog, openDialog, paintReveal, revealSecret, tokenValue } from "./admin-view.js"; +import { freshAdminAuth, mountAdminAccess, mountUserDashboardAccess } from "./admin-access.js"; import { loadSkills } from "./admin-skills.js"; import { mountSkillAssignmentPicker } from "./skill-assignment-picker.js"; import { mountUserPicker } from "./admin-user-picker.js"; @@ -71,7 +72,7 @@ let detailDirty = false; // whether the open conversation detail has unsaved edi // Controls that save through their OWN request are never part of a card's "Unsaved changes" state. // Environment secrets and VPN control have independent writes and must never round-trip through // the card's Save or tell the admin the card has edits waiting. -const SELF_SAVING_CONTROLS = ".channel-env-card, .ch-vpn-controls, .ch-codex-login"; +const SELF_SAVING_CONTROLS = ".channel-env-card, .ch-vpn-controls, .ch-codex-login, .admin-access-card"; const viewLoaded = {}; const EFFORT_OPTIONS = { @@ -128,14 +129,14 @@ function engineOptionsHtml({ includeDefault = false } = {}) { selectableEngines().map((m) => ``).join(""); } -function syncEffortOptions({ engineSelect, modelSelect, effortSelect, label, value }) { - const engine = effectiveEngine(engineSelect?.value || ""); +function syncEffortOptions({ engineSelect, modelSelect, effortSelect, label, value, engine: pinnedEngine, blankLabel = "default" }) { + const engine = pinnedEngine || effectiveEngine(engineSelect?.value || ""); const modelEfforts = MODEL_EFFORT_OPTIONS[engine]?.[modelSelect?.value || ""]; const options = modelEfforts?.length ? [["", "default"], ...modelEfforts.map((effort) => [effort, effort])] : EFFORT_OPTIONS[engine] || EFFORT_OPTIONS.claude; const current = value !== undefined ? value : effortSelect.value; - effortSelect.innerHTML = options.map(([v, text]) => ``).join(""); + effortSelect.innerHTML = options.map(([v, text]) => ``).join(""); effortSelect.value = options.some(([v]) => v === current) ? current : ""; if (label) label.textContent = `Effort (${ENGINE_MANIFESTS.find((m) => m.id === engine)?.label || engine})`; } @@ -187,6 +188,12 @@ function syncModelOptions({ engineSelect, modelSelect, value, engine, blankLabel modelSelect.value = options.some(([v]) => v === current) ? current : ""; } +function syncDefaultEffort(modelSelect, effortSelect, engine, value) { + const sync = (next) => syncEffortOptions({ modelSelect, effortSelect, engine, value: next, blankLabel: "CLI default" }); + sync(value || ""); + modelSelect.onchange = () => sync(effortSelect.value); +} + function addModelShortcutRow(name = "", target = {}) { const box = document.getElementById("model-shortcuts-editor"); const row = document.createElement("div"); @@ -342,11 +349,16 @@ function capLabelOf(m = {}) { } // ── View switching + URL history ──────────────────────────────────────────────── -function loadView(name) { +function loadView(name, { refreshIdentity = true } = {}) { if (name === "system-health") setSystemHealthActive(true); if (name === "channels" && !viewLoaded.channels) { viewLoaded.channels = true; loadConversations().catch(() => {}); } if (name === "users" && !viewLoaded.users) { viewLoaded.users = true; loadUsers().catch(() => {}); } - if (name === "settings" && !viewLoaded.settings) { viewLoaded.settings = true; loadSettings().catch(() => {}); } + if (name === "settings") { + if (!viewLoaded.settings) { viewLoaded.settings = true; loadSettings().catch(() => {}); } + // The self-saving identity card can change when chat discovers the first admin or a user + // role changes. Refresh it independently so existing unsaved Settings edits stay intact. + else if (refreshIdentity) mountAdminAccess(document.getElementById("admin-access"), { confirm: confirmDialog }).catch(() => {}); + } if (name === "api" && !viewLoaded.api) { viewLoaded.api = true; loadApiDocs().catch(() => {}); } if (name === "skills") loadSkills().catch(() => {}); // always refresh if (name === "schedules") loadSchedules().catch(() => {}); // always refresh @@ -399,11 +411,48 @@ async function loadApiDocs() { if (note) { note.textContent = hasKey === true - ? "A token is set. Reveal, copy, or regenerate it under Settings → HTTP run API." - : hasKey === false - ? "No token generated yet — create one under Settings → HTTP run API." - : "Generate one under Settings → HTTP run API."; - note.classList.toggle("apidoc-note-warn", hasKey === false); + ? "Channel tokens: ask the agent in a conversation for create_api_token. A deprecated organization-wide key is also set under Settings → HTTP run API." + : "Channel tokens: ask the agent in a conversation for create_api_token (listed below). No organization-wide key is set — good."; + note.classList.toggle("apidoc-note-warn", hasKey === true); + } + await paintApiTokens(); +} + +// Every channel token across conversations — names, conversation, creator, dates, last four +// characters — and a revoke. Values never reach this page (they exist once, in the minting reply). +async function paintApiTokens() { + const rows = document.getElementById("apitok-rows"); + if (!rows) return; + let tokens = []; + try { + tokens = (await api("/api/api-tokens")).tokens || []; + } catch { + rows.innerHTML = `Sign in to see channel tokens.`; + return; + } + const when = (v) => (v ? escapeHtml(String(v).slice(0, 16).replace("T", " ")) : "never"); + rows.innerHTML = tokens.length + ? tokens.map((t) => ` + ${escapeHtml(t.prefix)}…${escapeHtml(t.last4)} ${escapeHtml(t.name)}${t.revoked ? " (revoked)" : ""} + ${escapeHtml(t.channelName || t.slug)} + ${escapeHtml(t.createdBy)} + ${when(t.createdAt)} + ${when(t.lastUsedAt)} + ${t.revoked ? "" : ``} + `).join("") + : `No channel tokens yet. In a conversation, ask the agent: “create an API token for this channel”.`; + for (const btn of rows.querySelectorAll(".apitok-revoke")) { + btn.addEventListener("click", async () => { + btn.disabled = true; + try { + await api(`/api/api-tokens/${encodeURIComponent(btn.dataset.id)}/revoke`, { method: "POST", body: "{}" }); + } catch (e) { + btn.disabled = false; + alert(`Could not revoke: ${e?.message || e}`); + return; + } + await paintApiTokens(); + }); } } @@ -1223,6 +1272,7 @@ async function loadDashboard() {
      `; body.innerHTML = ` + ${d.historyDays ? `

      Free cost-tracking history covers the last ${fmtNum(d.historyDays)} days. Enterprise includes unlimited history.

      ` : ""}
      ${kpiHtml}
      ${modelLegend(keys)}
      @@ -1447,7 +1497,7 @@ function workDirConflictSummary(conflict) { const names = others.map((item) => item.isDM || item.type === "im" ? (item.name || item.slug) : hashName(item.name || item.slug)); - return `Working folder is also assigned to ${names.join(", ")}`; + return `Working folder setup is linked with ${names.join(", ")}`; } function convRow(key, color, name, sub, cost, conflict = null) { @@ -1459,10 +1509,10 @@ function convRow(key, color, name, sub, cost, conflict = null) { // Compact whole-dollar 30-day cost (skip sub-$1 rows so the list stays quiet). const dollars = cost == null ? null : Math.round(cost); const costHtml = dollars && dollars >= 1 ? `$${escapeHtml(dollars.toLocaleString())}` : ""; - const conflictHtml = conflictText ? '!' : ""; + const conflictHtml = conflictText ? '↔' : ""; el.innerHTML = `` + - `${escapeHtml(name)}${sub ? `${escapeHtml(sub)}` : ""}${conflictText ? 'Shared working folder' : ""}` + + `${escapeHtml(name)}${sub ? `${escapeHtml(sub)}` : ""}${conflictText ? 'Linked folder setup' : ""}` + conflictHtml + costHtml; el.addEventListener("click", (e) => { @@ -1499,7 +1549,7 @@ function renderConvList() { e.textContent = CHANNELS.length ? "No channels match." : "No channels yet — invite the bot and send a message."; list.appendChild(e); } else { - for (const c of chans) list.appendChild(convRow("ch:" + c.channelId, capColorOf(c.meta || {}), conversationChannelName(c), capLabelOf(c.meta || {}), costFor(c.channelId, c.slug), c.workDirConflict)); + for (const c of chans) list.appendChild(convRow("ch:" + c.channelId, capColorOf(c.meta || {}), conversationChannelName(c), c.meta?.archived ? "Archived" : capLabelOf(c.meta || {}), costFor(c.channelId, c.slug), c.workDirConflict)); } } @@ -2005,12 +2055,12 @@ function renderChannelDetail(ch) { const node = document.getElementById("channel-card").content.cloneNode(true); const card = node.querySelector(".conv-detail"); card.querySelector(".ch-name").textContent = conversationChannelName(ch); - card.querySelector(".ch-type").textContent = ch.type + (ch.isDM ? " · DM" : ""); + card.querySelector(".ch-type").textContent = ch.type + (ch.isDM ? " · DM" : "") + (meta.archived ? " · Archived — re-invite the bot to resume configuration updates" : ""); card.querySelector(".ch-slug").textContent = ch.slug; // Header capability pill — colored by the live mode; kept in sync as flags change. const modePill = card.querySelector(".ch-mode"); - const paintModePill = (m) => { modePill.textContent = capLabelOf(m); modePill.setAttribute("style", capPillStyle(m)); }; + const paintModePill = (m) => { modePill.textContent = meta.archived ? "Archived" : capLabelOf(m); modePill.setAttribute("style", capPillStyle(m)); }; paintModePill(meta); // ── Dirty tracking / savebar ────────────────────────────────────────────────── @@ -2086,7 +2136,7 @@ function renderChannelDetail(ch) { } const inherited = () => { const template = SKILL_TEMPLATES.find((t) => t.slug === templateSelect.value); - const section = catalog.skills.filter((s) => s.channelScope === ch.channelId && !s.deleted && s.visibility !== "personal").map((s) => s.slug); + const section = catalog.skills.filter((s) => s.channelScope === (meta.workspaceLink?.channelId || ch.channelId) && !s.deleted && s.visibility !== "personal").map((s) => s.slug); return [ { id: "organization", label: "Organization skills", slugs: overview.orgSkills || [], note: "Managed organization-wide." }, { id: "template", label: "Template skills", slugs: template?.resolved || [], note: template ? `Included by ${template.name}. Edit the template in Skills → Templates.` : "Choose a template above to include its skills." }, @@ -2094,7 +2144,8 @@ function renderChannelDetail(ch) { ]; }; skillsPicker = mountSkillAssignmentPicker(skillsBox, { skills: catalog.skills, sources: catalog.sources, selected: meta.skills || [], inherited: inherited(), onChange: markDirty }); - templateSelect.disabled = false; + templateSelect.disabled = !!meta.workspaceLink; + if (meta.workspaceLink) for (const input of skillsBox.querySelectorAll("input,button,select")) input.disabled = true; templateSelect.addEventListener("change", () => skillsPicker.update({ inherited: inherited() })); }).catch((error) => { if (!skillsBox.isConnected) return; @@ -2114,6 +2165,7 @@ function renderChannelDetail(ch) { flagEls.cleanMode.checked = !!meta.cleanMode; const networkBox = card.querySelector(".ch-network"); networkBox.checked = !!meta.allowNetwork; + showBudgetStatus(card.querySelector(".ch-budget-status"), ch.channelId); // Capability radio cards drive the hidden `; card.appendChild(tplField); const tplSel = tplField.querySelector(".dm-template"); - tplSel.value = dm.template; + tplSel.value = dm.meta?.workspaceLink ? "custom" : dm.template; + if (dm.meta?.workspaceLink) { + tplSel.disabled = true; + const linked = document.createElement("p"); + linked.className = "hint"; + linked.textContent = `Shared setup linked to ${dm.meta.workspaceLink.name}. Edit shared settings in that source conversation; harness, model and effort remain independent.`; + head.after(linked); + } + const budgetField = document.createElement("label"); + budgetField.className = "field"; + budgetField.innerHTML = `Monthly budget (USD) · EnterpriseBlank = unlimited. Resets monthly at 00:00 UTC. Active runs may exceed the limit; unpriced usage pauses new runs until pricing is repaired. External service charges are excluded.`; + budgetField.querySelector("input").value = dm.meta?.monthlySpendingLimitUSD ?? ""; + card.appendChild(budgetField); + showBudgetStatus(budgetField.querySelector(".dm-budget-status"), dm.channelId); const customBox = document.createElement("div"); customBox.className = "dm-custom"; card.appendChild(customBox); @@ -2879,7 +3010,13 @@ function renderDmDetail(id) { const renderCustom = () => { customBox.innerHTML = ""; ed = null; - if (tplSel.value === "custom") { ed = buildConfigEditor(dm.meta || {}); customBox.appendChild(ed.el); } + if (tplSel.value === "custom") { + ed = buildConfigEditor(dm.meta || {}); + customBox.appendChild(ed.el); + if (dm.meta?.workspaceLink) for (const input of ed.el.querySelectorAll("input,select,button")) { + if (!input.matches(".cfg-engine,.cfg-model,.cfg-effort")) input.disabled = true; + } + } }; renderCustom(); paintCap(tplSel.value); @@ -2891,9 +3028,12 @@ function renderDmDetail(id) { card, bar, async () => { const tpl = tplSel.value; - const body = { template: tpl, ...(tpl === "custom" && ed ? ed.getValues() : {}) }; + const values = tpl === "custom" && ed ? ed.getValues() : {}; + const body = { template: tpl, monthlySpendingLimitUSD: readBudgetInput(budgetField.querySelector("input")), ...Object.fromEntries(Object.entries(values).filter(([key]) => !dm.meta?.workspaceLink?.fields?.includes(key))) }; await api(`/api/dms/${encodeURIComponent(dm.channelId)}`, { method: "PUT", body: JSON.stringify(body) }); + showBudgetStatus(budgetField.querySelector(".dm-budget-status"), dm.channelId); dm.template = tpl; + dm.meta = { ...dm.meta, monthlySpendingLimitUSD: body.monthlySpendingLimitUSD }; renderConvList(); }, () => renderDmDetail(id), @@ -3425,6 +3565,8 @@ function openUserDrawer(id) { drawer.hidden = false; drawer.appendChild(node); + mountUserDashboardAccess(drawer.querySelector(".ud-dashboard-access"), id, { confirm: confirmDialog }); + // Editable masked token fields — same reveal semantics as before. attachReveal(drawer.querySelector(".ud-composio"), { has: u.hasComposioToken, last4: u.composioTokenLast4, fetch: revealSecret("user", "composioToken", id) }); attachReveal(drawer.querySelector(".ud-toolboxtoken"), { has: u.hasToolboxToken, last4: u.toolboxTokenLast4, fetch: revealSecret("user", "toolboxToken", id) }); @@ -3838,6 +3980,7 @@ function paintQwenProviders(providers) { const keyInput = at("api-key"); const urlInput = at("base-url"); const modelSelect = at("default-model"); + const effortSelect = at("default-effort"); const clearBtn = at("clear-key"); // An endpoint the provider ships is a placeholder (blank = keep it); one only the operator can // know is not, so the box says so instead of implying a default exists. @@ -3848,6 +3991,7 @@ function paintQwenProviders(providers) { // The button sits inside the field's
      - Auto-approval by company email domain + Auto-approval by company email domain · Enterprise

      Approve members automatically when their platform-provided email matches an exact domain. This grants Approved access, never Admin. Manual revocations prevent automatic reapproval. Removing a domain does not revoke existing approvals.

      @@ -140,7 +140,7 @@

      Users

      - +
      @@ -247,7 +247,12 @@

      Endpoints

      Authentication

      -

      Send the token you generated in Settings as either an X-API-Key header or an Authorization: Bearer header. The token is scoped to /api/runs only — it is not a general admin credential and can't read settings, tokens, or the filesystem browser. A logged-in admin session cookie also works from the browser.

      +

      Channel tokens are the way to authenticate: anyone working in a conversation asks the agent there to create_api_token (or uses list_api_tokens / revoke_api_token). Such a token belongs to that one conversation and to the person who created it: runs it starts are posted there, act with that person's current role and authorization (an admin's token can do what an admin can; a member's token is a member), and stop working the moment the token is revoked or the person loses access. Send it as X-API-Key or Authorization: Bearer. It is scoped to /api/runs and to its conversation only; channel may be omitted or must name that conversation, and author is ignored.

      +

      Deprecated — organization-wide key. The single key under Settings → HTTP run API still works for existing automations, acts as a fixed admin principal in any conversation, and logs a deprecation warning when used. Replace it with channel tokens and clear it.

      + + + +
      TokenConversationCreated byCreatedLast used
      Loading…
      @@ -436,6 +441,7 @@

      Settings

      Integrations License Access & security + Admin access System @@ -554,6 +560,15 @@

      Public URL

      Public URL (the daemon's externally-reachable base URL) + +

      Without this, every remote login shares one backoff bucket behind the proxy; with it, each real client gets its own. Only declare a proxy you control: it must set the header on every request.

      Approval links

      @@ -601,10 +616,20 @@

      Engine & runtime

      Default Claude model — pins gateway runs so the terminal's /model choice never leaks in + +
      @@ -706,8 +736,10 @@

      Message activation reactions

      Agent instructions

      -

      Google Drive sync

      +

      Google Drive sync · Enterprise

      Two-way sync (scheduled, via rclone bisync) between a per-channel Google Drive folder and that channel's whole working folder — except agent instructions and skills (CLAUDE.md, AGENTS.md, .claude/, …), channel memory, secrets (.env, keys), .git and dependency trees, plus anything listed in the folder's .driveignore. Symlinks are never followed, and rclone runs in a throwaway container that sees only the channel folder. Auth is a Workspace service account — drop its JSON key file on this host and point to it below. Set a channel's folder link under Conversations → the channel → Google Drive sync folder. rclone must be installed on this host (apt install rclone, or the official installer from rclone.org).

      - +
      +
      +
      +

      Admin access

      +

      Your passkeys and the gateway admin user they belong to. Invite other admins from Users.

      +
      +
      +
      +

      System

      -

      Admin UI password, the HTTP run API token, Slack connection controls and the danger zone.

      +

      The HTTP run API token, Slack connection controls and the danger zone.

      -

      Admin UI

      - -
      -
      -

      HTTP run API

      -

      A bearer token that lets automations (Make.com, cron, …) trigger runs via POST /api/runs without an admin login. Send it as an X-API-Key header or Authorization: Bearer — it's scoped to the run API only, not the rest of the admin API. Generate one, Save, then Copy it into your automation.

      +

      HTTP run API — deprecated organization-wide key

      +

      Deprecated: prefer channel tokens (ask the agent in a conversation for create_api_token; listed on the API page). This single key acts as a fixed admin in every conversation. A bearer token that lets automations (Make.com, cron, …) trigger runs via POST /api/runs without an admin login. Send it as an X-API-Key header or Authorization: Bearer — it's scoped to the run API only, not the rest of the admin API. Generate one, Save, then Copy it into your automation.

      -
      - Remove admin passwordThe UI becomes open — anyone who can reach it can administer. - -
      Restart daemonChoose to wait for ongoing work or force a restart and interrupt active turns and jobs. @@ -1374,7 +1403,7 @@

      Environment variables

      @@ -1386,7 +1415,7 @@

      Environment variables

      +