diff --git a/CHANGELOG.md b/CHANGELOG.md index bc49c00..4d8cc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,53 @@ product overview. > | Makeitfuture Sustainable Use License 1.1 | 2026-08-20 | never published | > | Makeitfuture Sustainable Use License 1.0 | 2026-08-06 | never published | +## 0.5.6 — 2026-09-25 + +- Codex over SSH now gets what a chat turn's Codex gets: the gateway tools, your own and the + channel's Composio accounts, the channel's MCP servers and your channel secrets, starting in + the channel folder. `with-secrets ` runs any other command with those secrets. +- Slack **⚙️ Settings** pages are tabs again: one row — General · Resume · MCP · Skills · Secrets — + with the open page highlighted, instead of the *Page* dropdown. +- Google Drive sync now syncs the channel's whole folder instead of a `Drive/` subfolder. Agent + instructions and skills, channel memory, secrets, `.git` and dependency trees never sync, a + `.driveignore` file adds a channel's own exclusions, and symlinks are never followed. rclone now + runs in a throwaway container that sees only the channel folder, so a planted link can never + write onto the host (this also closes that hole in the old subfolder sync). A folder that + contains the home or the gateway's data is refused. +- VS Code over SSH now starts in the channel's folder: File → Open Folder in a Remote-SSH window + opens at the channel folder (or its custom folder) instead of the container's home. +- Handing a generated file to Drive, Gmail or another Composio tool now goes through the gateway's + staging on every engine: the rule rides each run's Composio identity notice, so Codex no longer + falls back to pushing the file through Composio's workbench as base64. +- Google Drive sync no longer stalls on a folder that was empty when it was linked. The first sync + of two empty sides succeeded, but every later sync then failed (rclone: "Empty prior Path1 + listing"), so files never moved. A sync record that holds no files now triggers a fresh resync. +- Slack **⚙️ Settings** no longer claims a channel pinned to Claude inherits the Codex default + model (or the reverse): an unset model or effort now names what the channel's own harness would + actually use. The web admin's VPN status also stops saying "not configured" twice. + +- Sending a generated file to Drive, Gmail and other Composio tools works again — or rather, works + for the first time on a gateway using personal Composio tokens. `stage_file_for_composio` sent the + stored token to Composio's REST upload, which only accepts project API keys; the tokens stored + here are consumer (MCP) keys, so every stage failed with "Invalid API key" and the model told the + user their key was broken. With a consumer key the gateway now stages through the hosted MCP's own + workbench, daemon-side and in chunks, so the file still never passes through the conversation + (limit 25 MB there, 100 MB with a project key). A real problem now reads `Staging failed:`; + `Staging refused:` is kept for files that are not the conversation's own (outside its folder, a + symlink, not a regular file). +- Security: staging a file for Composio now reads the exact file whose location it checked. It + used to check the file, close it and open it again by name, and the conversation's container can + write its own folder — so in that gap the name could be swapped for a link to a file elsewhere on + the host, which the gateway would then have read and uploaded. Found by review during the + 2026-09-25 QA campaign. The tool first shipped in 0.5.3, and on a gateway using consumer keys (the + default) no stage could complete before this release, so the gap could not have been used there. +- The assistant no longer suggests a blanket container prune when asked about disk space. The + rule against `podman system prune`/`reset`, `podman image prune -a`, `podman volume prune` and + `docker system prune` lived only in the operating guide, and a run that answered without opening + the guide recommended two of them — on this host they can delete channel homes or the runtime + image. The rule now sits among the few hard rules every run of every engine reads, pointing at + `npm run runtime:storage` (removal only with `-- --apply` when an admin asks). + ## 0.5.5 — 2026-09-24 - The runtime image no longer carries SSH host private keys. Installing `openssh-server` generated diff --git a/FEATURES.md b/FEATURES.md index 450413b..d58ab67 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -364,9 +364,10 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. - **Slack settings for authorized users:** replies requested by anyone allowed to use the agent add a requester-bound **⚙️ Settings** footer button. Its Block Kit console mirrors the web setup concepts across five pages — **General Settings**, **Resume Session**, **MCP**, **Skills**, - **Secrets** — picked from a single *Page* dropdown rather than a row of buttons that wrapped onto - a second line as pages were added. Legacy page ids (`runtime`, `access`, `network`) still resolve, - so a Settings view opened before the merge keeps navigating. + **Secrets** — shown as one row of tabs (*General · Resume · MCP · Skills · Secrets*) at the top, + the open page highlighted; short names keep the five on one row of the modal. Legacy page ids + (`runtime`, `access`, `network`) and the former *Page* dropdown still resolve, so a Settings view + opened before either change keeps navigating. **General Settings** is everything that decides how the conversation runs: its Engine & model scopes, then **Access** (with the network switch and the VPN row). Engine & model edits in place — six dropdowns, no nested form, each saving the @@ -375,7 +376,9 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. inside a thread, **that thread's own pins** (the per-thread overrides the `/model` wizard's "just this thread" scope and the `claude`/`codex` directive write, which beat the channel at run time). An unset field preselects the label of what it inherits — the gateway default, an org DM - template's value, or "Follow channel (…)" for a thread — so nothing reads as a blank; each + template's value, or "Follow channel (…)" for a thread — so nothing reads as a blank (an unset + model or effort names what the channel's OWN harness would use, so a Claude-pinned channel on a + Codex-default gateway reads the Claude default, never the Codex one); each scope's model and effort lists follow the harness THAT scope resolves to, so a Codex-pinned thread inside a Claude channel offers Codex models. Changing one field drops only the dependents it invalidates (a harness change always clears the model, and the effort when the new harness @@ -1807,8 +1810,32 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. object to pass straight through. The caller names which identity will run the destination tool (`user` → `composio-user`, `agent` → `composio-agent`) and the key resolved for THAT identity is the one spent, through the same precedence the MCP config uses; a named identity with no key is - reported rather than silently replaced by the other one. The key never enters the container, - never reaches the model, and never appears in an error message. Nothing is published. + reported rather than silently replaced by the other one. Every run that has a Composio identity + is also told this handoff rule in its per-run identity line (not only in the gateway-usage guide), + so an engine that never opens the guide's sharing page still stages instead of relaying base64. + The key never enters the container, never reaches the model, and never appears in an error + message. Nothing is published. + The REST upload accepts only a Composio PROJECT API key. The keys this gateway stores in personal + mode are CONSUMER keys (`ck_…`, the hosted MCP's credential), which that endpoint rejects, so + with a consumer key the gateway stages through the hosted MCP's own workbench instead: the daemon + opens its own MCP session on the same key, writes the file into the sandbox in base64 chunks of + 768 KB (one request above ~5 MB is rejected), checks the md5 and mints the key with + `get_mount_file_s3_key` — a key the model's separate session can then use. Bytes still never + pass through the model. The sandbox path is a random directory plus a name reduced to safe + characters, so a file name can never become Python source. Consumer-key staging is capped at + 25 MB (project keys keep 100 MB), within an overall 10-minute deadline; the MCP session is ended + afterwards, but the sandbox copy is kept because the s3key IS that file's storage (deleting it + breaks the upload — verified live). A known property of this route: the copy stays in that + identity's Composio file storage, so on `composio-agent` backed by the organization key it is + listable from any conversation sharing that key (its random directory name is not a secret) — + stage on `composio-user`, or give the channel its own Composio key, when that matters. The bytes are read from the SAME descriptor the confinement + check proved (O_NOFOLLOW + /proc/self/fd), bounded to the cap: the tool used to close that + descriptor and reopen the file by path, so a symlink swapped in by the container between proof + and read could have sent any file the daemon can read. Every error text is scrubbed of the key + and of long base64 runs before it reaches the model. A path outside the working folder answers `Staging refused:`; + anything that goes wrong after that answers `Staging failed:`, and the guide tells the model a + refusal is final (no copying the file in to get around it) and a failure is reported, not + worked around by pushing the bytes through a tool itself. → TEST-PLAN: Composio file staging. - **Temporary public file links (`create_public_file_link`).** For destinations that ingest by URL rather than by body, and for a person who simply wants a link. One file from the channel's own @@ -2360,7 +2387,19 @@ are retired, bullet by bullet; everything else stands. the thread-bound background, progress and approval tools) with `composio-user` for the developer's own accounts, `composio-agent` for the channel's and the selected catalog servers, `--strict-mcp-config` so the operator's own claude.ai connectors never load, and the run - environment (`resolveRunEnv` + `safeSpawnEnv`) sourced by the `claude` wrapper. The files are per + environment (`resolveRunEnv` + `safeSpawnEnv`) sourced by the `claude` wrapper. Codex gets the + same session through its own `codex` wrapper (QA-0925: over SSH it had no gateway MCP, no Composio + and no secrets): the session also writes `codex-args.sh` — exactly the `-c mcp_servers.*` / + `apps.*` overrides a chat turn's Codex gets, with a Codex-minted gateway capability and the + Composio/toolbox credentials in a 0600 `codex-secrets.json` their header helpers read, never argv + — and the wrapper sources the secrets, prepends the overrides and, when started from HOME, `/` or + a parent of the channel folder, starts Codex in the channel folder (its `AGENTS.md`); a turn's + sandbox/approval flags are not carried, the developer answers Codex's own prompts. As in a turn, + `apps._default.enabled=false` keeps the shared Codex sign-in's ChatGPT connectors (`codex_apps`, + the operator's) off except apps the channel selected, and a selected server with no safe + definition is left out. `with-secrets ` runs any other command with the developer's channel + secrets. A Codex preparation failure is reported (`codexProblem` on `ssh_session_start`) and + never costs the Claude session. The files are per developer under `/ssh/users//`, chosen by the `CG_SSH_USER` name sshd sets from the developer's authorized_keys line (`PermitUserEnvironment CG_SSH_USER`), and removed when their last session ends, refreshed every 20 minutes AND within `CONFIG_REFRESH_DEBOUNCE_MS` of a @@ -2379,7 +2418,11 @@ are retired, bullet by bullet; everything else stands. container's namespaces; no container and no extra host port ever listens; the channel is named in the ProxyCommand, so one key reaches several channels at once. A container with a live session is never idle-stopped or evicted, and a rebuild waits for it like for a run; a dead peer - is reaped by `ClientAlive` in about three minutes. Sessions are `ssh_sessions` rows and + is reaped by `ClientAlive` in about three minutes. Each session also seeds VS Code's remote + machine setting `files.dialog.defaultPath` with the channel's effective work folder (a custom + folder included), so Remote-SSH's File → Open Folder starts there instead of `/home/agent`; + the merge is best effort, follows a changed folder, and never overrides a developer's own value. + Sessions are `ssh_sessions` rows and `ssh_session_start`/`ssh_session_end` events; refusals are `ssh_attach_refused` with the reason the developer saw. The image ships `openssh-server` (spec 1.4.0) and marks the `agent` account key-only (`*`, not useradd's locked `!`). It carries no SSH host private key: the package's @@ -2859,10 +2902,18 @@ are retired, bullet by bullet; everything else stands. rather than by reading the shared identity that holds other people's accounts (and the mirror: "your X" never touches `composio-user`); that `COMPOSIO_MANAGE_CONNECTIONS` initiates connections rather than listing them; and that only the gateway's `run_in_background` / - `run_agent_in_background` / `create_schedule` can report back after a turn ends — stated here + `run_agent_in_background` / `create_schedule` can report back after a turn ends; and that no run + runs or recommends `podman system prune`/`reset`, `podman image prune -a`, `podman volume prune` + or `docker system prune`, each of which can delete channel homes or the runtime image — disk + cleanup goes through `npm run runtime:storage`, with `-- --apply` only when an admin asks — stated here because a skill body is read only when the model opens it, and one engine reliably did not - (retest, 2026-09-06). They ride clean mode too, are engine-neutral, - and stay under 4 KB with the switches so the always-on prompt weight is read rather than skimmed. Editable three ways: the admin UI Instructions tab (edits the real + (retest, 2026-09-06; the prune rule after a 2026-09-25 run that skipped the skill and recommended + both prunes). They ride clean mode too, are engine-neutral, + and stay under 4 KB with the switches so the always-on prompt weight is read rather than skimmed + (4,085 bytes in the measured configuration after the prune rule — the header note, the rules' + intro and the network line were tightened to make room; the longest switch combination, Admin + + Auto with Lean off, is 4,120 bytes, down from 4,126 before, and a test stops it growing. The next + always-on rule has to trade space for it). Editable three ways: the admin UI Instructions tab (edits the real file; managed block shown read-only with a Settings link; hash-guarded against concurrent writes), by hand, or by asking the agent — the `update_channel_instructions` gateway MCP tool appends a rule in any mode (replace = admin-only). New sessions and `/clear` pick the file up @@ -2952,9 +3003,26 @@ are retired, bullet by bullet; everything else stands. pending (an armed *clear* toggle, a typed password or key), because a pending action captured as "already saved" would silently never run. → TEST-PLAN: Admin UI. - **Google Drive two-way sync (scheduled)**: a per-channel Drive folder link (channel settings) - is bisync'd on a timer into a dedicated `Drive/` subfolder of that channel's working folder — - never the folder root, so the confinement scaffolding (`.claude/`, `CLAUDE.md`, `MEMORY.md`, - `memory/`, `uploads/`) is never synced or overwritten. Auth is a Workspace service account + is bisync'd on a timer with that channel's WHOLE working folder (it used to be a `Drive/` + subfolder; QA-0925). A filters file, matched case-insensitively and applied in both directions, + keeps out agent instructions, skills and MCP config at any depth (`CLAUDE.md`, `AGENTS.md`, + `AGENTS.override.md`, `CLAUDE.local.md`, `.mcp.json`, `.claude/`, `.agents/`, `.codex/`), channel + memory (`MEMORY.md`, `memory/`), secrets (`.env*`, `.ssh/`, key and credential files, per-run env + folders), `.git` (file or folder), `.worktrees`, dependency trees and `*.rclonelink`, so a Drive + editor cannot plant instructions for the agent and no local secret is pushed. A `.driveignore` + at the folder root adds the channel's own exclusions (every line becomes an exclude). Every + symlink in the folder is excluded for the pass, and rclone runs in a throwaway container + (`podman run --rm`, all capabilities dropped) that mounts only the channel folder at its real + path and the sync state, filters (read-only) and key (read-only) at a random path per pass: a + link planted to anywhere else — even mid-pass — resolves inside that container and can never + write onto the host or into the sync state. A timed-out pass force-removes its container. A folder + that is or contains the operator's home, the gateway root or the workspace root, a hidden + configuration folder of the home (`~/.ssh`, `~/.config`, …), or a workspace folder that is not + this channel's own (another channel's, the platform parent, `.runtime`) is refused — so is a Lean + (clean-mode) channel, whose runs use a bare folder inside the gateway root. The first sync of a link merges both sides, + the newer copy winning where both hold the same path; the completed `--resync` records what it + was made for (local root, Drive folder, filters), and changing any of them makes the next pass a + fresh `--resync` from a clean state dir instead of a bisync against the old pair's listings. Auth is a Workspace service account (+ optional domain-wide-delegation subject), passed to `rclone bisync` via flags — no interactive `rclone config`, no per-user OAuth, no secret in the child env. The key is entered by **pasting the service-account JSON** into the settings page: stored write-only (validated as a real SA key, @@ -2987,7 +3055,13 @@ are retired, bullet by bullet; everything else stands. through daemon IPC (`drivesync` kind) so the pass runs in the daemon on both MCP transports, shares the per-channel in-flight guard and outlives the turn; it waits ~40 s for the outcome and otherwise says the pass is still running. `get_channel_drive_folder` now also reports the last - pass (time, ok/failed, first-resync, reason). Manual passes obey the same global switch, key and + pass (time, ok/failed, first-resync, reason). A pass whose prior rclone listings record no file + (including the empty listings rclone set aside as `.lst-err` after refusing them) runs as a + `--resync` again, so a folder linked while empty on both sides starts syncing as soon as either + side gets a file instead of failing every tick with rclone's exit 7. Any other missing listing + stays an error: after a deliberate delete-everything rclone sets aside NON-empty listings, and a + resync there would copy the deleted files back. + Manual passes obey the same global switch, key and rclone checks as the schedule; a pass already running for a channel is never doubled, and a manual sweep never stacks on the scheduled one. Status surfaces show a concise diagnostic, never the raw rclone tail. Admin API: `POST /api/channels/:id/sync-now`, `GET diff --git a/TEST-PLAN.md b/TEST-PLAN.md index 989460f..a198214 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -56,12 +56,41 @@ `agent` → the channel token), a named identity with no key is reported instead of falling back to the other, and a path escaping the channel folder is refused before any key is spent. Engine-independent: the tool runs daemon-side and no harness participates. +- [x] Staging race (`test/composio-files.test.js`): with the proven descriptor open, the file's path + is replaced by a symlink to a file outside the folder — the staged bytes are still the proven + file's; a direct caller handing a symlinked path gets ELOOP (O_NOFOLLOW); a file that grows + past the cap after the size check is refused without being read whole. Mutation-checked: + reintroducing the reopen-by-path, dropping the session header, accepting a server ping as the + answer, or removing the key scrub each turns a test red. +- [x] Consumer keys (`test/composio-files.test.js`, `test/file-sharing-tools.test.js`): a `ck_` key + never reaches the REST API; the file crosses as appended base64 chunks in one MCP session and + is md5-verified before `get_mount_file_s3_key`; an incomplete transfer, a sandbox error and an + oversize file (the 25 MB consumer cap) are errors without the key; a hostile file name never + reaches the Python source; a project (`ak_`) key keeps the REST route and any other shape is + treated as a consumer key; every later request carries the minted MCP session id and the + session is ended with DELETE; SSE replies parse and a server ping (id + method) is never taken + for the answer; zero-length files and exact chunk multiples reassemble; a JSON-RPC error, an + HTTP error, `isError`, `successful:false` with and without text, and a hung server (overall + deadline) are all errors; an upstream body echoing the key is scrubbed on both routes; an + upstream failure answers `Staging failed:` while only the confinement check answers + `Staging refused:`. +- [x] Live proof of the route (2026-09-25, Xavier): a `ck_` key is rejected by the REST upload under + both `x-api-key` and `x-consumer-api-key` (401). Through the hosted MCP on the same key: a 1 MB + file stages in one call; 4 MB is rejected (413); a 3 MB file sent as four 768 KB appends + reassembles with a matching md5; and an `s3key` minted in one MCP session uploads successfully + to Drive from a SEPARATE session (the daemon/model split). Probe file deleted afterwards. - [ ] Live acceptance, Claude and Codex: in a fixture channel with a Composio connection, ask the agent to put a file it generated into Drive. Require it to call `stage_file_for_composio` (not a base64 relay, not a public link), then `GOOGLEDRIVE_UPLOAD_FILE` on the SAME identity, and require the file to open in Drive with the right bytes. Repeat with "my Drive" vs "your Drive" and confirm the staged identity matches the one the upload ran as. Record the `composio_file_staged` audit event. +- [x] Automated: every per-run Composio identity line (both, user-only, agent-only) carries the + handoff rule — `stage_file_for_composio` with the identity that runs the destination tool, a + `create_public_file_link` upload link for URL-only tools, and never file bytes as base64 or + chunks through the workbench to get around staging — and an identity-less run gets no line + (`test/composio-identity-preamble.test.js`). QA-0925 FSHARE-02: a Codex turn that read the + guide's front page but not its sharing page base64'd a PDF through the workbench. ## Temporary public file links @@ -191,6 +220,14 @@ ## Host container-storage housekeeping guidance +- [x] `test/folders-generator-paths.test.js`: the managed CLAUDE.md block (read by every run of both + engines, skill opened or not) forbids running or recommending `podman system prune`/`reset`, + `podman image prune -a`, `podman volume prune` and `docker system prune`, each named whole on + one line, with a consequence true for all of them ("can delete channel homes or the runtime + image") and the admin path `npm run runtime:storage` (`-- --apply` only if an admin asks); the + header note keeps its three facts; the block stays under 4 KB in the measured configuration + and the worst switch combination may not exceed its current 4,120 bytes. Live: OPS-DISK-01 (Airtable) on both engines — a 2026-09-25 Claude run never + opened the skill and recommended both prunes; the Codex run read administration.md and did not. - [x] `test/host-housekeeping-guide.test.js`: the materialized guide for every platform routes disk/stale-container/old-image questions to `references/administration.md` and carries the instruction to report and ask, removing only via `--apply` on an admin's word, and forbids @@ -1934,13 +1971,20 @@ Automated: `test/channel-memory.test.js`, `test/memory-search.test.js`, (`test/slack-progress.test.js`, `test/channel-settings-modal.test.js`, `test/deliver.test.js`). - [ ] Live General Settings (engine-independent Slack UI case): in a disposable channel with a manager actor and an ordinary approved member, open **⚙️ Settings** from a reply. Pass when - the modal opens on General Settings with one *Page* dropdown listing five pages; switching - pages through it repaints in place; the manager sees Engine & model, Access (summary + + the modal opens on General Settings with one row of five tabs (General highlighted) that + fits on one line; clicking a tab repaints in place and highlights it; the manager sees Engine & model, Access (summary + *Change access settings*) and the VPN row under the network switch on one page, while the member sees the same page without the access summary and cannot reach the editor; saving access settings returns to General Settings with its notice; and a channel with a provisioned VPN shows *Checking status…* replaced by the real state, while a channel without one shows *Not configured* immediately with no flicker. +- [x] Automated inherited-model label (engine-independent): with the gateway default harness + Codex, a channel pinned to Claude with no model shows *Inherited default ()*, + not the Codex default, and offers only Claude models + (`test/channel-settings-modal.test.js`). The web admin's unconfigured VPN row explains the + missing setup once, without the server's own "VPN is not configured. …" repeated + (`test/channel-vpn-web.test.js`, browser case; run it from a path without a dot-directory — + the static server 404s any path under `.worktrees/`). - [ ] Live resume round trip (engine-independent Slack UI case, run once per harness where the session is minted by that harness): in a disposable channel, send a message, then open **⚙️ Settings → Resume Session** from a reply inside that thread. Pass when the tab shows the @@ -1949,10 +1993,12 @@ Automated: `test/channel-memory.test.js`, `test/memory-search.test.js`, pasting the same line back as `/resume ` continues it from the thread. Then `/clear` the thread and reopen the tab: it must say there is no session rather than offering the cleared id. -- [x] Automated page dropdown and General Settings: the modal carries exactly one *Page* control — - a `static_select` whose options are General Settings / Resume Session / MCP / Skills / - Secrets in that order, opening on the page being shown, each option bound to the view's - channel and owner, and no page rendered as a button any more. General Settings carries the +- [x] Automated page tabs and General Settings: the modal carries one `actions` row of tab buttons + General / Resume / MCP / Skills / Secrets in that order, above the page content, exactly the + open page styled primary, each bound to the view's channel and owner, and no page dropdown; + a picked option from a view opened while the dropdown shipped still switches pages + (`test/channel-settings-modal.test.js`). Earlier the pages were a *Page* dropdown (Tiberiu + asked for tabs back, QA-0925). General Settings carries the Engine & model and Access headers in that order, with the VPN rendered as a single row immediately after the Auto/Lean/Network checkboxes rather than a section of its own. Every legacy page id (`runtime`, `access`, `network`) and an unknown one resolve to `general`, so a Settings view @@ -3236,12 +3282,40 @@ structural invariants are automated; rendered navigation and feature claims also Test action no-op with a clear message and never throw (smoke-tested). - [ ] Manual (needs rclone + a Workspace service-account key): set the global key-file path + enable; set a channel's Drive folder link; click **Test** → "Connected". Then wait one - interval (or restart) → files appear in `/Drive/`; a local edit there - propagates up to Drive and a Drive edit propagates down, on the next tick. -- [ ] Confinement: the sync only ever writes under `Drive/` — `.claude/`, `CLAUDE.md`, `AGENTS.md`, - `MEMORY.md`, `memory/`, `uploads/` are never pushed to Drive nor overwritten from it. + interval (or restart) → the channel folder's files appear in the Drive folder and Drive files + appear in the channel folder; a local edit propagates up and a Drive edit down, on the next tick. +- [x] Confinement (`test/drivesync.test.js`, real rclone): the whole folder syncs, while `.claude/`, + `CLAUDE.md`, `MEMORY.md`, `memory/`, `runtime/env/` and `.env` never reach Drive and a Drive-side + `CLAUDE.md` never overwrites the channel's; `.driveignore` lines only ever add excludes; filters + carry `--ignore-case`; every symlink is excluded for the pass (names glob-escaped); a folder that + is or contains the home, the gateway root or the workspace root, a hidden folder of the home, or a + workspace folder other than this channel's own is refused; the pass is launched as `podman run + --rm --pull=never --cap-drop ALL --name cg-drivesync-…` with the work folder at its real path and + the state, read-only filters and read-only key at a fresh random `/cg-sync-` path per pass + (no host path of the state or key exists inside), and a timed-out pass force-removes it; a changed local root, Drive folder or filter set forces + a fresh `--resync` (a pre-identity sentinel from the `Drive/` subfolder era resyncs once). + Live-verified on Xavier (QA-0925 review): with a work-folder symlink to a host folder and a + Drive-side payload under it, the confined pass left the host folder empty even without the + symlink exclude; with it, the pass succeeded, `lnk2 -> .claude` could not overwrite `.claude/`, + and Drive-side `Claude.md` / `AGENTS.override.md` did not come down. +- [ ] Live (engine-independent): on a channel whose folder has a lockdown, memory and a `.env`, link + a Drive folder and **Sync now**. Pass: the Drive folder holds the channel's own files but none of + `.claude/`, `CLAUDE.md`, `AGENTS.md`, `MEMORY.md`, `memory/`, `.env`, `.git`; a file added in Drive + reaches the channel folder; a pattern added to `.driveignore` stops that path syncing. - [ ] A failed first run leaves no half-baked bisync state (the state dir is dropped, so the next tick retries with `--resync`). +- [x] A prior listing that records no file (`.lst`, or the `.lst-err` rclone set aside after refusing + it) forces `--resync` again, and any other missing listing stays an error + (`test/drivesync.test.js`, with real-rclone cases that skip when rclone is absent: an empty + folder that later gets a file syncs; deleting every file on one side is NOT undone by a + forced resync; a failed forced resync keeps its state). rclone's `--resync` of two EMPTY sides + succeeds but leaves an empty listing, and every later pass then aborted with exit 7 "Empty + prior Path1 listing … Must run --resync to recover". QA-0925: a channel linked to an empty + shared-drive folder synced "OK" once and then failed every tick once a file appeared. +- [ ] Live (engine-independent): link a channel to an EMPTY Drive folder with an empty local + `Drive/`, click **Sync now** (ok), then put a file in the local `Drive/` and click **Sync now** + again. Pass: the second pass succeeds and the file appears in the Drive folder; a file added + in Drive then reaches `Drive/` on the next pass; no `drivesync_error` event. - [x] Set the Drive folder link via the gateway MCP tools (`src/mcp/gateway-server.js`): `set_channel_drive_folder`/`clear_channel_drive_folder` are admin-gated (`requireAdmin`) and reuse the shared `parseDriveFolderId` (junk link → rejected before any write) + `testChannelSync` @@ -5369,6 +5443,27 @@ are the v0.8 production deployment gate and are executed in the QA loop that fol - [x] Unit: the image ships `cg-sshd` (POSIX sh clean) and the spec is 1.4.0 in both `containers/versions.json` and `image-paths.js` (automated: `test/container-image.test.js`, `test/container-durability.test.js`). +- [x] Automated (`test/ssh-session.test.js`): session prep runs the VS Code start-folder seed in the + channel's container with the effective work folder; the seed creates the machine settings, + keeps VS Code's own keys, follows a changed work folder, never overrides a developer-set + `files.dialog.defaultPath`, never rewrites a non-JSON file, and a failed seed never blocks the + session. QA-0925: Remote-SSH landed in `/home/agent`. +- [ ] LIVE (engine-independent): connect with VS Code Remote-SSH from its own menu to a channel with + a custom work folder. Pass: File → Open Folder opens at that folder (not `/home/agent`), OK + opens it, and a new terminal's `pwd` is that folder. +- [x] Automated (`test/ssh-session.test.js`): an SSH session also prepares Codex — `codex-args.sh` + (0600) holds only `-c mcp_servers.*`/`apps.*` overrides (gateway, composio-user, + composio-agent), no credential and no turn sandbox/approval flags; the 0600 `codex-secrets.json` + bundle carries a gateway capability minted for engine `codex` and both Composio tokens; the + `codex` and `with-secrets` wrappers are installed; run under `sh`, the codex wrapper sources the + secrets, prepends the overrides before the developer's own arguments and moves into the channel + folder only from HOME or a parent of it (never from elsewhere), while outside an SSH session it + is the plain CLI. QA-0925: Codex over SSH had none of it. +- [ ] LIVE (Codex): in a granted SSH session run `codex` from `~` and ask it to list + its MCP servers and whether `` is set (never its value). Pass: it starts + in the channel folder, `/mcp` lists `gateway`, `composio-user`, `composio-agent` (and the + channel's selected servers), the secret is present, and a gateway tool (e.g. list_secrets) + answers; `with-secrets sh -c 'test -n "$NAME" && echo set'` prints `set`. - [ ] LIVE (engine-independent, Airtable CTR-31): on the gateway host run `npm run build:image`, then `sudo CG_SSH_HOST= bash scripts/install-ssh-access.sh`; within a minute the daemon log shows `[ssh] attach socket`. As Apps, in `cg-testing-claude-bash`, send "add my SSH key CI passed on the exact candidate and a live smoke ran for Claude and Qwen. The owner released it > without Codex live acceptance (the Codex account was usage-limited until 2026-09-24) and without > the full live campaign; both remain open for the next release, see RELEASE-ACCEPTANCE.md. +> 0.5.6 was published on 2026-09-25 by explicit owner decision after the full automated gate and +> CI passed on the exact candidate. Live on Xavier before the cut (QA-0925, Airtable): the Composio +> staging route for consumer keys (FSHARE-02 both engines, after the Codex handoff-rule fix), the +> Drive empty-listing resync (the wedged channel recovered and synced), Codex over SSH with the +> gateway, Composio and secrets (owner-verified), SSH governance SSH-GOV-01 (shell) and -03. One SSH +> hardening item was deferred by owner decision and is tracked in the private QA base. The +> whole-folder Drive sync's first pass on the one linked channel was still uploading at the cut +> (its timeout cleanup verified live); the remaining QA-0925 cases continue after release. > 0.5.5 was published on 2026-09-24 by explicit owner decision after the full automated gate and > CI passed on the exact candidate. It exists to produce the first signed release artifacts since > 0.5.2: the `v0.5.3` Release evidence run failed its artifact scan (seven findings in the image diff --git a/docs/SSH-ACCESS.md b/docs/SSH-ACCESS.md index 562b643..e192e73 100644 --- a/docs/SSH-ACCESS.md +++ b/docs/SSH-ACCESS.md @@ -93,13 +93,24 @@ yet, and `show_channel_ssh` names the installer. Remote-SSH → `acme-app`. The channel rides in the ProxyCommand: one key, one config block per channel, several channels at once. The first connection records the channel's own host key. -To open VS Code directly on the channel folder rather than browsing from `/home/agent`, use the -command "show SSH access" prints: `code --remote ssh-remote+acme-app `. +To open VS Code directly on the channel folder, use the command "show SSH access" prints: +`code --remote ssh-remote+acme-app `. Connecting from Remote-SSH's own menu opens an +empty window instead; its File → Open Folder dialog starts in the channel folder (a custom work +folder included), because every session seeds `files.dialog.defaultPath` in the container's VS Code +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). -Codex uses the shared sign-in mount. Everyone in a container is that one `agent` user: set your +Codex uses the shared sign-in mount, 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 +`composio-agent`, the channel's selected MCP servers and your channel secrets. Started from your +home, `/` or a parent of the channel folder it moves into the channel folder so its `AGENTS.md` +applies. Like Claude's claude.ai connectors, the ChatGPT connectors of the shared Codex sign-in +(`codex_apps`) are off in a session except apps the channel selected — they belong to the +operator's account. For any other command, `with-secrets ` +runs it with your channel secrets (bare `with-secrets` prints how to load them into the shell). +Everyone in a container is that one `agent` user: set your git identity per session, and expect to see other sessions' processes. A daemon restart drops brokered sessions — reconnect. diff --git a/package-lock.json b/package-lock.json index b85220c..c654321 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "channelgate", - "version": "0.5.5", + "version": "0.5.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "channelgate", - "version": "0.5.5", + "version": "0.5.6", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@composio/core": "0.14.0", diff --git a/package.json b/package.json index d2d369e..e3fd833 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "channelgate", - "version": "0.5.5", + "version": "0.5.6", "private": true, "license": "SEE LICENSE IN LICENSE.md", "author": "Tiberiu Socaci (MAKEITFUTURE S.R.L.)", diff --git a/public/app.js b/public/app.js index c28ad07..39e6c7b 100644 --- a/public/app.js +++ b/public/app.js @@ -1771,7 +1771,11 @@ function mountChannelVpnControls(card, channelId) { refresh.disabled = pending; if (snapshot) { const labels = { unconfigured: "Not configured", unavailable: "Unavailable", off: "Off", starting: "Starting", on: "Connected", stopping: "Stopping", failed: "Failed" }; - const parts = [labels[snapshot.state] || "Unknown", snapshot.message]; + // The state word, plus the server's message only where the word cannot say why on its own (a + // failure, an unavailable service) — for the other states it restates the label, and for an + // unconfigured VPN it repeated the hint below almost word for word. + const explained = ["failed", "unavailable"].includes(snapshot.state) ? snapshot.message : ""; + const parts = [labels[snapshot.state] || "Unknown", explained]; if (!snapshot.configured) parts.push("An administrator must import the VPN profile and prepare the channel’s VPN service first."); if (snapshot.missingSecrets?.length) parts.push(`Add in Environment: ${snapshot.missingSecrets.join(", ")}.`); if (snapshot.configured && !snapshot.allowNetwork) parts.push("Enable Network and save the channel before starting VPN."); diff --git a/public/index.html b/public/index.html index 0f81f37..5c7e100 100644 --- a/public/index.html +++ b/public/index.html @@ -688,7 +688,7 @@

Public file links

Google Drive sync

-

Two-way sync (scheduled, via rclone bisync) between a per-channel Google Drive folder and a dedicated Drive/ subfolder inside that channel's working 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).

+

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