diff --git a/CLAUDE.md b/CLAUDE.md index 35e7e9880..e0c8b0954 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Guidance for Claude Code (claude.ai/code) when working in this repository. This Winter is a macOS-native AI assistant: a TypeScript/Bun daemon (`winter-core`) plus a native Swift menu-bar app. They speak JSON-RPC 2.0 over NDJSON on a Unix socket (`/run/core.sock`; default home `~/.winter`). The daemon is the single source of truth; every client — CLI, Mac app, phone — is a view over its event stream. -**The daemon runs no agent loop of its own.** Every session is a runtime child driven through the router (`@yanlinglabs/winter-runtime-sdk`) on the Winter agent runtime — the only leg; every model runs on it, Claude included. Code sessions spawn the `winter` binary; chat and dispatch run it EMBEDDED in `winter-core`, one Bun Worker per session. Session modes are `code`, `dispatch`, `chat`. **Cowork is not an implemented session mode** — the protocol names it (`SESSION_MODES`) but `session.create` refuses it for every role (`unsupported_topology`) and no runtime-facing type can hold it; see [docs/cowork-status.md](docs/cowork-status.md). The daemon owns everything around the child: IPC, event-sourced session storage, settings, approvals, the capability tools it hands the child over MCP, the hooks, and the SDK-message→`SessionEvent` projector. +**The daemon runs no agent loop of its own.** Every session is a runtime child driven through the router (`@yanlinglabs/winter-runtime-sdk`) on the Winter agent runtime — the only leg; every model runs on it, Claude included. Code sessions spawn the `winter` binary; chat and dispatch run it EMBEDDED in `winter-core`, one Bun Worker per session. Session modes are `code`, `dispatch`, `chat`. **Cowork is not an implemented session mode** — the protocol names it (`SESSION_MODES`) but `session.create` refuses it for every role (`unsupported_topology`) and no runtime-facing type can hold it; its native VM/guest source is inert and has never booted; see [docs/cowork-status.md](docs/cowork-status.md). The daemon owns everything around the child: IPC, event-sourced session storage, settings, approvals, the capability tools it hands the child over MCP, the hooks, and the SDK-message→`SessionEvent` projector. ## Commands @@ -57,7 +57,7 @@ Plain links, not `@` imports — read the file whose area you are about to touch | [providers-and-credentials.md](docs/claude/providers-and-credentials.md) | Before touching a Keychain slot, `credential.*`/`provider.*` doors, the host-credential broker, the boot Keychain passes or Anthropic/Console sign-in. | | [internal-jobs.md](docs/claude/internal-jobs.md) | Before touching titles, the bash safety reviewer, the dreamer or the session cleaner — model/effort/credential resolution and the role problems. | | [protocol-and-remote.md](docs/claude/protocol-and-remote.md) | Before changing a `SessionEvent` variant or field (the 7-step checklist), adding an RPC method, or touching what a remote/phone client sees (allowlists, history, transients, thinking pill, phone mirror). | -| [settings-and-migrations.md](docs/claude/settings-and-migrations.md) | Before adding a setting (live reload, the one-reader rule) or touching the legacy `~/.norma` home or project migration. | +| [settings-and-migrations.md](docs/claude/settings-and-migrations.md) | Before adding a setting (live reload, the one-reader rule), touching the legacy `~/.norma` home or project migration, or bumping the runtime-state schema (currently v21). | | [runtime-home.md](docs/claude/runtime-home.md) | Before touching `/sdk`, run folders, MCP/settings scopes and trust, Migration C, saved answers or the plugin format. | | [tool-surface.md](docs/claude/tool-surface.md) | Before changing what a model can call: capability servers and plain names, deferral, chat/dispatch allowed lists, web tools, hooks, read/write fences, sandbox escapes, saved allow rules, skills. | | [dispatch-and-messaging.md](docs/claude/dispatch-and-messaging.md) | Before touching mid-turn pushes (the fold), turn pairing, `SpawnSession`/children, `SendMessage`/`ListAgents`/`TaskStop`, `ListSessions`, or the Dispatch pill's command mode and terminal. | @@ -69,8 +69,8 @@ Plain links, not `@` imports — read the file whose area you are about to touch | [headless.md](docs/claude/headless.md) | Before touching the `headless` backend, the Winter Headless helper, its local-network rules or the Chromium embed layout. | | [usage.md](docs/claude/usage.md) | Before touching usage and cost tracking: `packages/core/src/usage/`, the projector's `onTurnUsage`, `usage.*`, `settings.usage`, reading the user's Codex/Claude Code/Antigravity/OpenCode usage or plans, `apple/Winter/Sources/Usage/`. | | [testing.md](docs/claude/testing.md) | Before running or writing tests, a verify gate, the live computer-use suite, or anything reading daemon logs/temp dirs. | -| [release.md](docs/claude/release.md) | Before bumping a version, writing release notes or cutting a release. | -| [cowork.md](docs/claude/cowork.md) | Before touching `packages/core/src/cowork/**`, `packages/protocol/src/cowork.ts`, `apple/Cowork/**`, host-storage-fence or home-fence cowork roots, `refreshForStorage`, or the `workspace.*` / `project.*` methods and events. | +| [release.md](docs/claude/release.md) | Before bumping a version, writing release notes or cutting a release — including the Cowork native-pair choice (`COWORK_NATIVE=omit` or both libkrun inputs) and a schema bump's rollback line. | +| [cowork.md](docs/claude/cowork.md) | Before touching `packages/core/src/cowork/**`, `packages/protocol/src/cowork.ts`, `apple/Cowork/**`, `scripts/cowork-guest-artifact/**`, host-storage-fence or home-fence cowork roots, `refreshForStorage`, or the `workspace.*` / `project.*` methods and events. | Other references: `winter-vs-cc-tools.md` (repo root, the live tool comparison with Claude Code), `apple/ComputerUse/PROTOCOL.md` (the helper's wire contract), `apple/ComputerUse/WinterBrowserHost/PROTOCOL.md` (Winter for Chrome's). diff --git a/docs/RELEASE-NOTES-cowork-schema-v18.md b/docs/RELEASE-NOTES-cowork-schema-v18.md deleted file mode 100644 index dd87a4d43..000000000 --- a/docs/RELEASE-NOTES-cowork-schema-v18.md +++ /dev/null @@ -1,7 +0,0 @@ -# Runtime-state schema v18 (Cowork tables) - release note draft - -Draft line for the release that first carries the Cowork integration (Cowork itself stays off): - -> Winter's local database (`runtimes/runtime-state.db`) moves to schema v18 on first launch. Going back to an older Winter afterwards needs one step first: quit Winter, run `winter migrate --cowork-schema --rollback` with this version's command-line tool, then install the older app. Skipping the step makes the older Winter refuse to open your sessions until the database is rolled back. - -Facts for the writer: the rollback accepts v8 to v18 and drops only empty Cowork tables (it refuses with `cowork-data-present` if one has a row, which no production build can write); nothing else in the database changes. See `docs/claude/settings-and-migrations.md` (Runtime-state schema v18 and the way back). diff --git a/docs/RELEASE-NOTES-cowork-schema.md b/docs/RELEASE-NOTES-cowork-schema.md new file mode 100644 index 000000000..0018e308c --- /dev/null +++ b/docs/RELEASE-NOTES-cowork-schema.md @@ -0,0 +1,9 @@ +# Runtime-state schema v21 (Cowork tables) - release note draft + +The version numbers below are pinned against `RUNTIME_STATE_SCHEMA_VERSION` (`packages/core/src/runtime-state/db.ts`); change them with it (main's `test/cowork/schema-docs-pin.test.ts` holds them once this branch lands). + +Draft line for the release that first carries the Cowork integration (Cowork itself stays off): + +> Winter's local database (`runtimes/runtime-state.db`) moves to schema v21 on first launch. Going back to an older Winter afterwards needs one step first: quit Winter, run `winter migrate --cowork-schema --rollback` with this version's command-line tool, then install the older app. Skipping the step makes the older Winter refuse to open your sessions until the database is rolled back. + +Facts for the writer: the rollback accepts v8 to v21 and drops only empty Cowork tables (it refuses with `cowork-data-present` if one has a row, which no production build can write); nothing else in the database changes. The step is needed for every store at v8 or higher, so an intermediate integration build (v18, v19 or v20) needs it too. See `docs/claude/settings-and-migrations.md` (Runtime-state schema v21 and the way back) for what v18 to v21 each add. diff --git a/docs/claude/cowork.md b/docs/claude/cowork.md index f1a730454..9e36ed3fa 100644 --- a/docs/claude/cowork.md +++ b/docs/claude/cowork.md @@ -2,7 +2,26 @@ *Read this before touching `packages/core/src/cowork/**`, `packages/protocol/src/cowork.ts`, `apple/Cowork/**`, the host-storage fence or `home-fence`'s Cowork roots, `refreshForStorage`, or the `workspace.*` / `project.*` methods and events.* -Cowork is not an implemented session mode: no Cowork session can be created or run, and `SpawnSession` refuses `type: "cowork"`. The protocol RECOGNISES the name (`SESSION_MODES`) so every door can refuse it typed: `session.create {mode: "cowork"}` answers `unsupported_topology` for every role, and a stored cowork row is refused rather than read as Code (see [runtime-and-sessions.md](./runtime-and-sessions.md)); there is deliberately no Cowork prompt, tool allowlist or run home. The host side exists: workspace metadata, guidance, schedules and outputs. What exists and what still blocks a functional mode: [cowork-status.md](../cowork-status.md). The design: [cowork-spec.md](../cowork-spec.md). +Cowork is not an implemented session mode: no Cowork session can be created or run, and `SpawnSession` refuses `type: "cowork"`. The protocol RECOGNISES the name (`SESSION_MODES`) so every door can refuse it typed: `session.create {mode: "cowork"}` answers `unsupported_topology` for every role, and a stored cowork row is refused rather than read as Code (see [runtime-and-sessions.md](./runtime-and-sessions.md)); there is deliberately no Cowork prompt, tool allowlist or run home. The host side exists: workspace metadata, guidance, schedules, outputs, the session-door records and (inert) the native worker, libkrun and guest-image source ([below](#native-guest-stack-on-the-integration-branch-source-only-never-booted)). What exists and what still blocks a functional mode: [cowork-status.md](../cowork-status.md). The design: [cowork-spec.md](../cowork-spec.md). + +## Native guest stack on the integration branch (source only, never booted) + +`cowork/output-integration` now holds the whole native source stack, and none of it runs: the activation seam (`cowork/native-activation.ts`, `COWORK_NATIVE_ENVIRONMENT_ACTIVATED = false`) answers `undefined`, the host-env graph loads lazily behind it (`test/cowork/host-env-lazy.test.ts`), and no VM, Hypervisor.framework call or app launch has ever been made from it. Wire contract and revision history: [cowork-native-worker.md](../cowork-native-worker.md) (revision 3 added host-backed shares, cold resume and the guest file channel). There is no NIC and no vsock anywhere in it. + +- **libkrun stack (SH2).** `apple/Cowork/WinterCoworkLibkrun` is a pinned patch queue (0004-0026) over upstream libkrun v1.19.6 with a reproducible build of one stripped archive that the worker links. Patches 0024-0026 are SH2: the confined virtio-fs server core, its git rules and quarantine, and the guarded fs device with the shares ABI gate. Nothing in the repository imports the directory (a test pins that); its README holds the pins and the rebuild steps. A release ships the worker only with the archive inputs ([release.md](./release.md#the-cowork-native-pair-is-an-explicit-choice)). +- **Host-backed shares over virtio-fs (SH6, guest side).** PID 1 (`apple/Cowork/WinterCoworkGuest/init/shares.c`) reads the virtio-fs tag inventory (it must equal the declared tags), mounts each share with `nosuid,nodev` and `dax=never` on a checked mount point's descriptor, then verifies every mount in `/proc/self/mountinfo`; any failure unmounts what it mounted. Three Winter shares are fixed: guidance at `/run/winter/shared-instructions` (read-only; PID 1 also links `/workspace/.winter/WINTER.md` to it), memory at `/workspace/.winter/memory` and outputs at `/workspace/outputs`, all `noexec`. A connected Mac folder is a guest share at `/workspace/folders/` (mode 0700 mount point, `exec` kept because builds run there, read-only when the share is). Guest ownership and modes on a share are no boundary: the host serves shares under simplified ownership. +- **Plan view.** For a conversation under policy `plan`, PID 1 makes `/workspace/plan//` (contract 12.5) for its own conversation only: owned by the conversation's tool uid alone (0700, never the workspace group), parents never symlinks, refused when a later epoch already exists. The guest supervisor admits it writable only under `plan` and never below an epoch already made; the tool launcher mounts it writable for the file worker only. +- **Environment image `/etc/gitconfig`.** The image carries a fixed system gitconfig (`scripts/cowork-guest-artifact/environment/gitconfig`, hash pinned in `pins.json`): an empty git template directory, `core.hooksPath=/dev/null`, no fsmonitor, no credential helper, no include, `protocol.ext.allow=never`. The tool launcher mounts it read-only at `/etc/gitconfig` for the process worker; `verify.py` accepts exactly those settings and refuses a stray `/etc/gitconfig`. +- **Memory root.** The memory share (`/workspace/.winter/memory`) is a host-query tool root of the guest runtime (agent SDK SH6b); the host side is the `memory` role in `WinterCoworkVM`'s `SharePolicy`. +- **Cold resume.** Host half: `cowork/cold-resume.ts` maps the recorded `store_state` (`none`, `started`, `created`, `lost`) to a continuation (`fresh`, `either`, `resume`, refuse) and the proven-ended boots of the last incarnation (runtime-state schema v21). Guest half: the session-owner patch (agent SDK #50) is in the pinned SDK commit. The host port stays GATED OFF: no production composition supplies `CoworkSessionDriverDeps.coldResume`, so a conversation with a recorded backend still refuses `cold_resume_unavailable`. Supplying it is the owner's flip. +- **Reproducible pinned guest image** (`scripts/cowork-guest-artifact/`). `pins.json` pins the agent SDK commit `ec91fe06` (plan view, shares, environment gitconfig, cold-resume owner), the expected runtime/cpio/root-manifest digests and the helper, init and relay binary digests, as produced by build 5 (two identical builds, both receipts compared). `kernel/pins.json` pins the Linux 6.18.35 `Image` and `vmlinux`, which two builds under different work paths reproduced byte for byte (status `built-reproduced-unbooted`). The runtime image digest is SHA-256 of the raw kernel-`Image` digest followed by the raw cpio digest (`runtime_image_digest.py`; the worker and launcher recompute it): `runtimeImageDigest` `da1795da4155dca3d40efda00aab2eab87fe6a7c3cc6175078396cff5a86ca65`. A build whose digests differ from the pins stops with exit 3 UNPINNED (the one-run workflow still uploads the image, receipts and `digests.json`, then fails its assert step). Build 6 (the one-run workflow on `cowork/guest-image-build-6`, from integration `cfd80390`; build 5's branch is spent) rebuilt the image with exit [0,0], every pin holding and both builds identical, and the same `runtimeImageDigest`, so the pinned image is confirmed reproducible from integration. Still nothing has booted. + +### Not done + +- `session.create {mode: "cowork"}` still answers `unsupported_topology` for every role; nothing creates or runs a Cowork session. +- The native egress broker and the provider bridge (mux class 3) are held, owner's work; they are not described here. +- Live gates G0-G9 have not run. Nothing has booted; G1 is the first boot, and every gate needs a review of the exact commit and the owner's approval. +- The cold-resume port, the activation seam and the session door's switch stay off until the owner flips them. ## Ordinary host execution policy @@ -39,10 +58,10 @@ Historical persistent-Code-floor reports describe the superseded approach. None of this makes Cowork runnable; there is still no creation door. Sentences followed are quoted in the code and tests. - **Backup exclusion** is the raw-volume primitive's (`cowork/volumes.ts`, `volume-backup.ts`): a sticky `tmutil addexclusion` (never `-p`) on each image when it is created, re-verified on every claim and open; shared instructions, memory and outputs get none. `test/cowork/volume-backup-adapter.test.ts` pins the real adapter on disposable files. -- **Image pins and export generations** (`cowork/images.ts`, `cowork/exports.ts`, `cowork/storage-lifecycle.ts`, runtime-state schema 16): trusted-controller classes with a mandatory authority port, no RPC. A pin records the base and runtime image digests and walks an update through `beginUpdate` then `confirmUpdate` or `abandonUpdate`, only while stopped and unowned, with a mandatory compatibility port; the live digests do not change until confirmed. An export generation is a recorded, manifest-bound descriptor of a stopped, ready, pinned workspace. `validateImport` judges a presented descriptor against canonical records and grants nothing. No bytes are copied, no snapshot is taken, and no restore runs. +- **Image pins and export generations** (`cowork/images.ts`, `cowork/exports.ts`, `cowork/storage-lifecycle.ts`, runtime-state schema 16): trusted-controller classes with a mandatory authority port, no RPC. A pin records the base and runtime image digests and walks an update through `beginUpdate` then `confirmUpdate` or `abandonUpdate`, only while stopped and unowned, with a mandatory compatibility port; the live digests do not change until confirmed. An export generation is a recorded, manifest-bound descriptor of a stopped, ready, pinned workspace. `validateImport` judges a presented descriptor against canonical records and grants nothing. Content-out (PR #97) added the byte-copying `.winterworkspace` writer (`export-writer.ts`, `export-bundle.ts`, with a two-pass shared snapshot) and an in-place restore of a stopped workspace's two images from a verified generation (`restore-bundle.ts`), all behind ports: no RPC reaches them and no production composition runs them. - **Migration B** never copies Cowork storage (`migration/cowork-exclusion.ts`): `cowork/**` is skipped, and a source holding any Cowork tree or populated Cowork table refuses with `cowork_storage_present` (the daemon's boot hook logs one skip line instead). Only the coordinated, quiescent migration the contracts describe could move it, and that does not exist yet. - **Migration C** never names Cowork storage: its moves are the fixed `SDK_COMPAT_LINKS`, `hostRuntimePredicate` keeps guest sessions out of runtime-state rewrites, a workspace that is not `stopped` or a volume that records an owner refuses it, and a manifest naming a `cowork/` path refuses resume, phase 2 and rollback. A rollback with populated Cowork records is refused by the schema step. - **Retention**: deleting a standalone workspace's only conversation tombstones its binding and, when the workspace is retained and stopped, archives it in the same transaction ("Detach/archive its workspace for explicit later deletion/export"): wake and disk admission refuse until an explicit restore, and both volumes and every shared file stay untouched. A Project workspace is unaffected, and a workspace that is not stopped stays retained. That transition lives in `abandonConversation`, and every route that retires a session reaches it: through `createCoworkSessionDeletionHook` (the boot reaper in `daemon.ts`, the mint-time reaper in `ipc/server.ts`, the cleaner's `onDelete`, the `session.create` refusal rollbacks, and the dispatch-children spawn-failure rollback) and directly from the missing-session branch of `CoworkWorkspaceEvents.drain`. None of them removes a volume or a shared file; they delete a session, tombstone its binding, and archive a stopped standalone workspace. **Owner decision 2026-10-16: keep archive-on-any-deletion, no behavior change.** The owner treats a workspace's own state (memory dir, temp dir, scratch, the VM disks) as temporary. It is removed only by the explicit, confirmed workspace delete (not built yet); the reapers, the cleaner and retention archive without removing anything, consistent with `cowork-spec.md` ("Scratch and outputs survive ordinary stop/archive"). Connected Mac folders and GitHub repositories are not part of the workspace and are never cleared. Archive is a state flip that deletes nothing, so automatic archive from every trigger above is acceptable. `test/cowork/storage-retention.test.ts` pins the behavior and ratchets the destructive primitives allowed under `src/cowork`. - **Owner decisions 2026-10-16** (detail in [cowork-storage-security.md](../cowork-storage-security.md#owner-decisions-2026-10-16); the slices are not built): workspace delete removes the VM disks plus the workspace's shared guidance, memory and outputs, only after a confirmation that lists them and offers an export first, never touches connected Mac folders or GitHub remotes, and warns when a GitHub-backed clone on `data.ext4` has unpushed commits or uncommitted changes; an export is a `.winterworkspace` folder bundle (the intended implementation, not part of the decision: the closed manifest plus sparse-copied images, cloned with APFS `clonefile` where possible); VM disks stay excluded from Time Machine and export is the backup path. -- **Still open (technical, not yet built)**: the `.winterworkspace` bundle writer; the shared-file snapshot (a proposal, not contract text: clone where possible, otherwise a double-pass hash, refusing the export if the passes differ); the restore flow (re-recording device/inode after a restore, an approved empty replacement through the existing ApprovalBroker card); the `workspace.archive`/`restore`/`delete` operations and `workspace.export.*` RPCs; wiring the image pin into admission with the native controller slice; and a Cowork-aware Migration B (until then it keeps refusing). +- **Still open (technical)**: the confirmed workspace delete; the `workspace.archive`/`restore`/`delete` operations and `workspace.export.*` RPCs that would reach the writer and restore above; wiring the image pin into admission with the native controller slice; and a Cowork-aware Migration B (until then it keeps refusing). diff --git a/docs/claude/release.md b/docs/claude/release.md index 6c8726dd6..b73b98189 100644 --- a/docs/claude/release.md +++ b/docs/claude/release.md @@ -16,5 +16,15 @@ bun run scripts/release.ts # real release (bumps first; -- - Pipeline: `scripts/release.ts` + `scripts/release-lib.ts`; `packaging/winter.rb.tmpl` is the Homebrew cask template it renders. - `releases/notes/.md` is REQUIRED — the GitHub release body and the Sparkle appcast ``, rendered by `releaseNotesHtml`, which throws (naming the line) on anything outside `#`/`##`/`###`, paragraphs, `- ` bullets with indented continuations (no nesting), ``` fences, `` `code` `` and `**bold**`. The preflight fails BEFORE the bump when it is absent: write and commit the notes for the next version, bump and sync, release with `--no-bump`. +## The Cowork native pair is an explicit choice +`release.ts` refuses to start a build until it knows whether the release ships the Cowork native pair (`coworkNativeReleasePlan` in `scripts/cowork-native-lib.ts`, read from `process.env` right after the output folder is cleared and before xcodegen or any build; `--dry-run` included). The pair is the signed launcher (`winter-cowork-launcher`) and VM worker (`winter-cowork-worker`) the daemon would spawn; shipping it activates nothing (the daemon still refuses Cowork sessions). + +| Environment | Mode | What the release gets | +|---|---|---| +| `COWORK_LIBKRUN_INCLUDE` (the directory holding `libkrun.h`) AND `COWORK_LIBKRUN_ARCHIVE` (the stripped production archive) | build | `buildCoworkNative` builds the launcher (`swift build`) and the worker (`apple/Cowork/WinterCoworkHVFWorker/build.py`, which refuses unless the header and archive hash to that folder's `pins.json`) into `dist/cowork-native`; project.yml's "Embed Cowork native" phase (`scripts/embed-cowork-native.sh`) signs them into `Winter.app/Contents/Helpers/WinterCowork/` and writes `Contents/Resources/cowork-native-pins.json`. The release then verifies identifiers, team, hardened runtime, the stated designated requirement, entitlements (worker: the hypervisor key alone; launcher: none), platform dylibs only, no test hooks, and that the pin manifest's CDHashes and libkrun/worker/guest-artifact pins are the ones in this checkout. | +| `COWORK_NATIVE=omit` (and neither libkrun variable) | omit | Nothing is built or embedded: no `WinterCowork` helpers, no pins file. The release fails if a leftover from an earlier build is in the bundle (`checkCoworkNativeOmitted`). | + +Anything else exits before any build with a message naming both ways out: neither given; only one of the two libkrun variables; `COWORK_NATIVE=omit` together with either libkrun variable; or any other value of `COWORK_NATIVE`. `omit` is never the default. `release.ts` does not build or fetch the libkrun archive; the patch queue and its reproducible build are in `apple/Cowork/WinterCoworkLibkrun` (`scripts/build_archive.py`), and its stripped archive is what `COWORK_LIBKRUN_ARCHIVE` points at. A release that must not wait on one uses `COWORK_NATIVE=omit`. `scripts/build-cowork-native.ts` reads the same two variables (or `--libkrun-include` / `--libkrun-archive`; `--launcher-only` skips the worker), and `embed-cowork-native.sh` itself also honours `COWORK_NATIVE=omit` and rejects any other value. + ## Rolling back across a schema bump -A release that raises `RUNTIME_STATE_SCHEMA_VERSION` (v18 added the Cowork tables) cannot be undone by installing the older app: the older build refuses the newer store. A revert release reverts the feature, never the schema. The user-facing way back is `winter migrate --cowork-schema --rollback` with the newer CLI and the daemon stopped, before installing the older app; put that line in the release notes of any release that bumps the schema (see `docs/RELEASE-NOTES-cowork-schema-v18.md`). +A release that raises `RUNTIME_STATE_SCHEMA_VERSION` (the Cowork tables arrived in v8 to v17; v18 added the session binding, v19 the session reservations, v20 the export content binding, v21 the stable guest uids and cold-resume records; it is v21 now) cannot be undone by installing the older app: the older build refuses the newer store (`newer-schema`). A revert release reverts the feature, never the schema. The user-facing way back is `winter migrate --cowork-schema --rollback` with the newer CLI and the daemon stopped, before installing the older app (it accepts v8..v21, drops only empty Cowork tables and refuses with `cowork-data-present` if one holds a row); a store left at v18, v19 or v20 by an intermediate build needs the same step. Put that line in the release notes of any release that bumps the schema (see `docs/RELEASE-NOTES-cowork-schema.md`; what each version adds: [settings-and-migrations.md](./settings-and-migrations.md#runtime-state-schema-v21-and-the-way-back)). diff --git a/docs/claude/settings-and-migrations.md b/docs/claude/settings-and-migrations.md index ab72e1178..4b750a90d 100644 --- a/docs/claude/settings-and-migrations.md +++ b/docs/claude/settings-and-migrations.md @@ -16,9 +16,13 @@ - Cowork storage is never copied (`migration/cowork-exclusion.ts`): `cowork/**` classifies as skipped, `planMigrationB`/`resumeMigrationB` refuse with `cowork_storage_present` when the source holds a Cowork tree or a populated Cowork table in its `runtime-state.db` (inspected through a private copy, never in place), the executor and rollback each refuse Cowork paths independently, and the daemon's boot hook logs one skip line rather than refusing to boot. [cowork.md](./cowork.md#storage-lifecycle-records-and-guards-only). - `/migration/manifest.json` is written atomically per step; a half-migrated home refuses boot `home_half_migrated` until `--resume`/`--rollback` (never auto-resumed). CLI: `winter migrate [--from ] [--status|--resume|--rollback] [--yes]` (`packages/cli/src/commands/migrate.ts`). -## Runtime-state schema v18 and the way back -- `runtimes/runtime-state.db` is at schema v18 (`RUNTIME_STATE_SCHEMA_VERSION`). Migration 18 (`rebuildsParentTable`) rebuilds `cowork_conversations` and adds `cowork_home_binding`; every daemon runs the whole ladder on upgrade, Cowork or not. The tables stay empty in production (nothing writes a Cowork row; pinned by `test/cowork/boot-quiet.test.ts`). -- **Rollback is one-way without one step.** An older Winter (0.124.0, schema v7) that meets a v18 store reports `newer-schema` and refuses EVERY session (`winter_leg_unavailable`). To go back: stop the daemon, run `winter migrate --cowork-schema --rollback` with the NEWER Winter's CLI (`downgradeCoworkRuntimeStateToV7`, accepts v8..v18; it drops the empty Cowork tables and sets v7), THEN install the older app. The command refuses with `cowork-data-present` only if a Cowork table holds a row, which a normal user never has. A revert release reverts the FEATURE, never the schema. +## Runtime-state schema v21 and the way back +- `runtimes/runtime-state.db` is at schema v21 (`RUNTIME_STATE_SCHEMA_VERSION`; main's `test/cowork/schema-docs-pin.test.ts` pins this doc and `docs/RELEASE-NOTES-cowork-schema.md` to it). Every daemon runs the whole ladder on upgrade, Cowork or not, and every Cowork table stays empty in production (nothing writes a Cowork row; pinned by `test/cowork/boot-quiet.test.ts`). What the last four steps add, all records only (`cowork/schema.ts`): + - **v18** (`rebuildsParentTable`): rebuilds `cowork_conversations` with `location_kind` and `backend_session_id`, and adds the write-once `cowork_home_binding`. + - **v19**: `cowork_session_reservations`, the durable first step of a Cowork `session.create`. + - **v20**: `cowork_exports.content_digest` and `cowork_export_output_ids` (content-out). + - **v21**: stable guest uids and the cold-resume records. `cowork_conversations` gains `runtime_uid`, `tool_uid`, `store_state` and `last_incarnation_id`; `cowork_workspaces` gains `store_volume_uuid` and `browse_uid`; new tables `cowork_guest_uids` and `cowork_guest_boots`; `cowork_peer_inbox` gains `offer_count`, `acked_hold_token`, `commit_confirmed_at` and `commit_outcome`. Pre-v21 conversations that already had a guest store get no uid rows (a documented gap that touches only dev stores; such a store is recreated, never migrated). +- **Rollback is one-way without one step.** An older Winter (0.124.0, schema v7) that meets a v21 store reports `newer-schema` and refuses EVERY session (`winter_leg_unavailable`). To go back: stop the daemon, run `winter migrate --cowork-schema --rollback` with the NEWER Winter's CLI (`downgradeCoworkRuntimeStateToV7`, accepts v8..v21; it drops the empty Cowork tables and sets v7), THEN install the older app. The same applies to a store left at v18, v19 or v20 by an intermediate integration build. The command refuses with `cowork-data-present` only if a Cowork table (`COWORK_TABLES`; the inbox sequence counters aside) holds a row, which a normal user never has; the Migration C step back to v6 (`winter migrate --sdk-home --rollback`) also accepts v7..v21 and also refuses a populated Cowork table. A store newer than v21 is refused by both. A revert release reverts the FEATURE, never the schema. - `winter doctor` on a newer-schema store says the same (the finding text names the command); an older build cannot say it, which is why the release notes carry it. ## Legacy projects diff --git a/docs/cowork-status.md b/docs/cowork-status.md index 3d1e77709..4a8c2db38 100644 --- a/docs/cowork-status.md +++ b/docs/cowork-status.md @@ -1,6 +1,6 @@ # Cowork implementation map -As of 2026-10-15, Winter cannot create or run a supported Cowork session. Code, Chat, and Dispatch are the implemented daemon modes. `SessionModeSchema` ([events.ts](../packages/protocol/src/events.ts)) now has four values, `code`, `dispatch`, `cowork` and `chat`: Cowork exists as a recognised wire and stored mode that the daemon REFUSES everywhere (`session.create` answers `unsupported_topology`, a stored cowork row never runs), and the runtime SDK's `RunMode` still excludes it. Cowork has host-side workspace infrastructure, Mac screens for workspaces, shared guidance and schedules ([later status](#later-status-2026-10-15)), visible UI placeholders for the composer and Dispatch `/spawn`, reserved names, and reusable infrastructure. A reserved predicate or a passing synthetic test does not make it a working product mode. +As of 2026-10-25 (first written 2026-10-15), Winter cannot create or run a supported Cowork session. Code, Chat, and Dispatch are the implemented daemon modes. `SessionModeSchema` ([events.ts](../packages/protocol/src/events.ts)) now has four values, `code`, `dispatch`, `cowork` and `chat`: Cowork exists as a recognised wire and stored mode that the daemon REFUSES everywhere (`session.create` answers `unsupported_topology`, a stored cowork row never runs), and the runtime SDK's `RunMode` still excludes it. Cowork has host-side workspace infrastructure, Mac screens for workspaces, shared guidance and schedules ([later status](#later-status-2026-10-15)), visible UI placeholders for the composer and Dispatch `/spawn`, reserved names, and reusable infrastructure. A reserved predicate or a passing synthetic test does not make it a working product mode. The 2026-10-12 accepted native-host policy preserves ordinary Code/tool execution without a persistent outer floor or first-activation restart debt. Core file checks and the existing single Bash sandbox protect registered paths; approved escapes keep their normal approval behavior with static write-shaped Cowork integrity checks. Activation refreshes Code incarnations/run homes while preserving queued messages and logical identity, and uses acknowledged live updates for Chat/Dispatch with the independently accepted SDK v1 nine-family context contract, preserving ongoing turns. Protected cold startup waits for its matching Worker acknowledgment. The installed SDK pin remains unchanged and lacks the contract; that version still requires existing embedded Workers to close and refuses protected Chat/Dispatch. Explicit owned-worker/bridge floors and host credential protection remain requirements; separate per-workspace encryption is deferred for the first version. Private images are plaintext, protected offline by FileVault only when enabled, and readable by same-user processes or approved escaped commands, which run as the user; these changes do not pass the production Cowork gate. @@ -22,6 +22,20 @@ The 2026-10-12 text above (except the opening paragraph, which is re-dated and e - **The runtime SDK** has, as of 2026-10-16, 17 merged PRs (#1-#17: guest transport, plain-guidance admission, structured instruction reads, source-only reader tooling, and on 2026-10-16 the reader packet, dependency probe and single-run hosted harness, #17, `29eede4b`). Its `RunMode` still excludes Cowork. Merged is not released or pinned in Winter. - **Still open:** supported creation, the Cowork prompt and tool surface, the composer send, folder and approval chips, session windows, Dispatch `/spawn`, and any iOS surface (the iOS app is unchanged since 2026-10-10). +## Later status (2026-10-25, `cowork/output-integration`) + +Everything below is source on the integration branch; none of it runs, and nothing has booted. + +- **Still true: no Cowork session can be created.** `session.create {mode: "cowork"}` still answers `unsupported_topology` for every role. The session door's records exist (runtime-state schema v18 to v21: binding location, reservations, export content binding, stable guest uids and cold-resume records; see [settings-and-migrations.md](claude/settings-and-migrations.md#runtime-state-schema-v21-and-the-way-back)) but no door is switched on, and the native activation seam (`cowork/native-activation.ts`) answers `undefined`. +- **Native source now exists end to end, unwired.** The Swift launcher (`apple/Cowork/WinterCoworkLauncher`), the C VM worker (`apple/Cowork/WinterCoworkHVFWorker`), the host adapter and wire contract (`packages/core/src/cowork/native/`, [cowork-native-worker.md](cowork-native-worker.md), revision 3), the guest PID 1, relay and file channel (`apple/Cowork/WinterCoworkGuest`) and a pinned libkrun patch queue (`apple/Cowork/WinterCoworkLibkrun`, patches 0004-0026 over libkrun v1.19.6). The release script requires an explicit choice: `COWORK_NATIVE=omit` or both libkrun inputs ([release.md](claude/release.md#the-cowork-native-pair-is-an-explicit-choice)). +- **libkrun SH2:** the confined virtio-fs server core (0024), its git rules and quarantine (0025), and the guarded fs device with the shares ABI gate (0026). +- **Guest side of host-backed shares (SH6)** and the **plan view** `/workspace/plan//` are in the guest init; Mac folders are guest shares at `/workspace/folders/` over virtio-fs; the memory share is at `/workspace/.winter/memory`; the image carries a fixed `/etc/gitconfig` for the process worker. Details: [cowork.md](claude/cowork.md#native-guest-stack-on-the-integration-branch-source-only-never-booted). +- **Cold resume:** the host half (store state, continuation, proven-ended boots) and the guest owner patch are both present; the host port is not supplied by any production composition, so a recorded-backend conversation still refuses `cold_resume_unavailable`. +- **Reproducible pinned guest image:** build 5 repinned the expected artifact (SDK `ec91fe06`), and the kernel `Image` and `vmlinux` are pinned from build 4. `runtimeImageDigest` is `da1795da4155dca3d40efda00aab2eab87fe6a7c3cc6175078396cff5a86ca65`. The kernel pins' status is `built-reproduced-unbooted`. Build 6 (the one-run workflow on `cowork/guest-image-build-6`, from integration `cfd80390`) rebuilt the image with exit [0,0], every pin holding and both builds identical, and `runtimeImageDigest` again `da1795da4155dca3d40efda00aab2eab87fe6a7c3cc6175078396cff5a86ca65`: the pinned image is confirmed reproducible from integration. Nothing has booted. +- **Held, owner's work:** the native egress broker and the provider bridge (mux class 3). They are not described here and nothing in this branch builds on them. +- **Live gates G0-G9 have not run.** Nothing has booted; G1 is the first boot. Every gate needs a review of the exact commit and the owner's approval. +- **Still open from before:** the Cowork prompt and tool surface wired to a runtime mode, the composer send, folder and approval chips, session windows, Dispatch `/spawn`, any iOS surface, and the `workspace.archive`/`restore`/`delete` and `workspace.export.*` RPCs (the `.winterworkspace` writer and restore exist behind ports with no RPC). + ## What already exists | Area | Implemented today | Cowork boundary | Source | @@ -30,7 +44,7 @@ The 2026-10-12 text above (except the opening paragraph, which is re-dated and e | Mac composer preview | Chat/Cowork selector, Cowork idea prefills, dedicated composer styling, and folder/approval chips. Cowork can be selected for preview. | Sending is blocked; the folder and approval chips are not wired. | [NewChatPage](../apple/Winter/Sources/AppShell/NewChatPage.swift), [ComposerChrome](../apple/Winter/Sources/AppShell/ComposerChrome.swift) | | Dispatch command | `/spawn` parsing, an unavailable notice, and an optional `onSpawnCowork` callback. | Production wiring does not set that callback. Its test invokes an injected closure, not a Cowork window. | [DispatchPillController](../apple/Winter/Sources/DispatchPill/DispatchPillController.swift), [controller tests](../apple/Winter/Tests/WinterAppTests/DispatchPillControllerTests.swift) | | Child orchestration | Dispatch creates Code sessions, tracks their work, relays cards, and receives completion updates. | `SpawnSession` accepts the reserved `cowork` argument only to reject it before creating anything. | [spawn schema](../packages/core/src/agent/tools/session-spawn.ts), [DispatchChildren](../packages/core/src/agent/dispatch-children.ts) | -| Guest conversation transport | Fixed host-record-derived launch descriptors and bounded SDK process-stream bridge, with conservative conversation lifetime retention. | No production native supervisor/ownership adapter; the actual SDK still refuses confined guest startup. Fixture frame/continuation mechanics are tested, not real durable guest execution. | [supervisor](../packages/core/src/cowork/guest-runtime.ts), [tests](../packages/core/test/cowork/guest-runtime.test.ts) | +| Guest conversation transport *(updated 2026-10-25)* | Fixed host-record-derived launch descriptors and bounded SDK process-stream bridge, with conservative conversation lifetime retention. | The native host adapter, launcher, worker and guest source now exist but are inert (the activation seam is off) and have never booted; nothing runs a real durable guest. Fixture frame/continuation mechanics are tested. | [supervisor](../packages/core/src/cowork/guest-runtime.ts), [tests](../packages/core/test/cowork/guest-runtime.test.ts) | | Lifecycle | Active/background/idle/archived derivation and setters are implemented. `participatesInActivity` includes `code` and the reserved `cowork` value. | Supported work sessions are Code. Cowork-shaped setter fixtures bypass supported creation. | [activity](../packages/core/src/sessions/activity.ts), [setActivity](../packages/core/src/sessions/set-activity.ts), [tests](../packages/core/test/ipc/session-set-activity.test.ts) | | Working directories | Ordered directories, canonical paths, locking, and the shared `setSessionDirs` operation. | It shares the reserved lifecycle predicate; no Cowork folder picker is connected to a real session. | [dirs](../packages/core/src/sessions/dirs.ts), [setDirs](../packages/core/src/sessions/set-dirs.ts), [tests](../packages/core/test/ipc/session-set-dirs.test.ts) | | Outputs | Per-session output paths, artifact enumeration, an outputs box, and side-viewer integration. | The eligibility helper accepts `cowork` for future use; live sessions reach this through Code. | [OutputsBox](../apple/Winter/Sources/AppShell/OutputsBox.swift), [ShellSessionHost](../apple/Winter/Sources/AppShell/ShellSessionHost.swift), [tests](../apple/Winter/Tests/WinterAppTests/OutputsBoxTests.swift) |