diff --git a/specs/contextcake-project-profiles/adversarial-review.md b/specs/contextcake-project-profiles/adversarial-review.md new file mode 100644 index 0000000..5540ad6 --- /dev/null +++ b/specs/contextcake-project-profiles/adversarial-review.md @@ -0,0 +1,159 @@ +# ContextCake Project Profiles — Adversarial Review Record + +**Date:** 2026-07-28 +**Artifacts reviewed:** `spec.md`, `design.md` +**Method:** independent read-only critic plus primary-agent red-team pass +**Review posture:** poke holes first; prioritize isolation, migration, privacy, +write safety, backward compatibility, and testability +**Outcome:** all seven blocking findings were accepted and incorporated into the +planning artifacts; implementation evidence does not exist yet + +--- + +## 1. Blocking Findings and Resolutions + +### B1. Synced incomplete profiles could invalidate healthy local use + +**Finding:** Account sync currently reconstructs profile layers after scrubbing +machine-local paths and commands. The draft simultaneously allowed these +incomplete shells and required whole-manifest validation before any source +opened. + +**Impact:** Pulling settings on a new Mac could make the complete manifest +unusable, including a healthy local default profile. + +**Resolution:** Accepted. Canonical profiles now have explicit +`pendingSources[]`. Incomplete or untrusted synced descriptors are structurally +valid repair tasks but are never passed to adapters, selection, MCP, sync, or +live-layer discovery. Migration and tests cover a healthy local profile beside +an incomplete remote shell. + +### B2. Synced `activeProfile` could remotely activate local executable sources + +**Finding:** `activeProfile` is currently syncable. Merge-by-source identity can +preserve a local MCP command underneath remotely scrubbed metadata. + +**Impact:** A remote snapshot, account switch, or id collision could select a +local profile and spawn its MCP command without fresh local intent. + +**Resolution:** Accepted. The active app profile is now local-only. Executable +source trust is a non-synced, machine-local digest bound to profile id, source +identity, command, arguments, and optional account owner. Copies, remote shells, +config changes, and account changes require confirmation again. + +### B3. Shipped hybrid manifests had no upgrade path + +**Finding:** The first draft rejected any document with both top-level `layers` +and `profiles`, but current Pack/settings-sync behavior can already produce and +test that transitional shape. + +**Impact:** A supported current configuration could fail on upgrade and strand +Pack assignments, pending sources, or synced metadata. + +**Resolution:** Accepted. The selector recognizes a transitional hybrid mode and +continues current flat-stack behavior until deliberate normalization. The atomic +migration preserves named profiles, owner metadata, pending sources, runnable +local fields, and Pack assignments. Real current-version manifests join the +upgrade fixture corpus. + +### B4. Profile/revision identity covered only part of the Console load + +**Finding:** The Console loads graph and resolved concepts through separate +requests. Adding profile identity only to `/api/graph` still permits mixed +generations, and mutations targeting the merely “active” profile can race a +switch. + +**Impact:** The UI could display topology from one profile with content from +another or mutate the wrong stack. + +**Resolution:** Accepted. Every profile-derived response carries +`{profileId, manifestRevision}`. Multi-request loads commit only when every +response matches the expected pair. Every profile/source/Pack/mapping mutation +names a profile and supplies a revision precondition; stale operations return +409. + +### B5. Cache isolation omitted configuration identity and clone state + +**Finding:** Profile plus source name is insufficient. Current cache paths use +source names, and clone-backed GitHub storage uses a repository slug without ref +or profile identity. + +**Impact:** Renamed/reconfigured sources or the same repository at different +refs could share stale or wrong content across profiles. + +**Resolution:** Accepted. The plan now hashes profile id plus the canonical +source configuration—kind, root/endpoint/repository, ref, path selection, and +adapter options—into every memory, disk, remote API, and clone namespace. Tests +cover same-name/different-source and same-repo/different-ref cases. + +### B6. A one-time fixed backup could become stale or corrupt + +**Finding:** Creating one never-overwritten migration backup allows a failed +first attempt to leave an old or partial file that later appears authoritative. + +**Impact:** Manual recovery could discard newer configuration. + +**Resolution:** Accepted. Backups are atomic, timestamped, and named/recorded +with the source manifest SHA-256. Recovery verifies the content hash and warns +that changes after the timestamp will be lost. No backup is restored +automatically. + +### B7. Promotion remained path-based and outside profile isolation + +**Finding:** The draft required selected-profile promotion, but the current CLI +accepts arbitrary source and target roots without reading a manifest. + +**Impact:** The requirement was unenforceable; a caller could promote between +unrelated profile roots. + +**Resolution:** Accepted. The plan adds a profile-aware promotion entry point +that resolves live and target layers by identity from one selected manifest and +revalidates before writing. Raw-path promotion remains an explicitly separate +legacy/advanced mode without a profile-isolation claim. + +## 2. Non-Blocking Findings and Resolutions + +| Finding | Disposition | +|---|---| +| Legacy `--profile default` reason contradicted the test matrix | Fixed: explicit override reports `explicit`; no override reports `legacy-default`. | +| Dangling-reference validation classes were ambiguous | Fixed: fatal structure, inactive warning, and selected-fatal classes are explicit. | +| Pack registry/layer invariants were incomplete | Fixed: bidirectional assignment/layer/version/origin/level validation and fixtures added. | +| Automatic CWD routing lacked a five-client proof matrix | Fixed: every advertised client must be tested; unsupported clients are labeled Default/explicit rather than automatic. | +| Profile labels lacked bounds and hostile-input handling | Fixed: NFC, trimming, 1–80 character bound, control/line-break and credential-pattern rejection. | + +## 3. Suggestions Applied + +- Added a sanitized real-world upgrade corpus from current Pack, team-sync, + signed-in sync, pending-source, and older desktop manifests. +- Made dogfood reproducible: at least 30 starts, five per profile, ten per + harness, failed starts retained, timestamped local scoring. +- Added deletion language explaining that running agents can retain snapshot + read access until restart; capture/promotion writes revalidate and fail after + profile deletion or live-layer change. + +## 4. Additional Primary-Agent Amendments + +The primary red-team pass also tightened two areas adjacent to the independent +findings: + +- Equal-level sources remain explicit tie groups so visual reordering does not + silently erase the existing recency tie-break rule. +- Pending synced sources and executable trust are separated: source metadata can + sync, but runnable commands and local activation cannot. + +## 5. Rejected Findings + +None. Every material finding was within the requested feature scope and improved +the safety or testability of the plan. + +## 6. Review Exit Criteria + +The planning review is closed because: + +- every blocking finding has a concrete requirement, design response, test, or + rollout gate; +- no unresolved marker or blocking product question remains; +- the plan preserves the approved integration, auth, Packs, distribution, and + team-sync boundaries; +- remaining risk is implementation risk, to be handled by per-PR review and the + seven-day dogfood gate rather than hidden in the planning document. diff --git a/specs/contextcake-project-profiles/design.md b/specs/contextcake-project-profiles/design.md new file mode 100644 index 0000000..1cb1562 --- /dev/null +++ b/specs/contextcake-project-profiles/design.md @@ -0,0 +1,915 @@ +# ContextCake Project Profiles — Delivery Plan + +How to deliver the approved profile model as one safe vertical slice across the +dependency-free engine, MCP server, engine service, Mac app, Console, Packs, +team sync, docs, and release validation. + +**Date:** 2026-07-28 +**Status:** Proposed — adversarial review complete; awaiting product approval +**Spec:** `specs/contextcake-project-profiles/spec.md` +**Estimated shape:** five implementation PRs plus a seven-day dogfood gate + +--- + +## 1. Outcome + +At the end of this plan, a user installs ContextCake once, connects each AI +harness once, and creates profiles such as `ContextCake`, `Mind Your Blanks`, +and `Work`. Each profile contains only its relevant local folders, GitHub +sources, Packs, and optional team live layer. Starting an agent inside a mapped +folder automatically selects the matching profile. + +The release is complete only when the selection is visible, deterministic, and +used consistently by every read and write path. A UI-only switcher over the +existing flat manifest is not sufficient. + +## 2. Current State and Gaps + +### Already present + +- `files`, `okf-local`, MCP, cache, and git-sync source machinery. +- A Mac app with a bearer-protected loopback engine service, native folder + picker, first-run source setup, CLI installation, and optional account sync. +- One global `contextcake mcp` registration flow for Claude Code, Codex, + Cursor, Claude Desktop, and generic MCP clients. +- Pack validation, immutable retained versions, rollback, registry assignments, + and CLI arguments that already accept `--profile`. +- Team-sync live capture, promotion, and content-free telemetry. +- An in-flight GitHub API adapter that follows the integration contract. + +### Missing or incomplete + +- `resolver.mjs`, `mcp-server.mjs`, and `service.mjs` still build only + top-level `manifest.layers`. +- `resolveLiveLayer()` still searches only the flat layer array. +- Pack management can write profile layers, but the runtime cannot resolve + them. +- Manifest reads and mutations are duplicated; Pack writes are locked and + atomic, while service source CRUD currently writes directly. +- The Console has source setup but no profile list, active-profile state, + project mappings, or scoped source management. +- Desktop settings sync understands profile-shaped metadata but project-path + mappings need an explicit never-sync rule. +- Current settings sync can persist a transitional document containing both + top-level `layers` and `profiles`, and can reconstruct scrubbed profile layers + without their runnable path/command. Upgrade logic must accept and normalize + that shipped shape rather than declaring it corrupt. +- `activeProfile` is currently a sync field. It must become local-only before a + profile id can select locally trusted executable MCP sources. +- Cache directories are keyed by source name and can collide when two profiles + reuse a layer name. +- GitHub source setup currently has two meanings: the shipped service clones a + repo into a local cache, while the in-flight adapter reads GitHub's API. The + UI must not present those as one indistinguishable source. + +## 3. Non-Negotiable Invariants + +1. **Select once.** Manifest validation and profile selection happen before any + adapter, live layer, Pack assignment, or telemetry writer is initialized. +2. **One stack per process/request context.** No operation receives the full + manifest after selection unless it is explicitly a configuration-management + operation. +3. **Fail closed on an intended-but-invalid match.** An explicit or matched + invalid profile never falls through to unrelated default context. +4. **Legacy is a first-class mode.** Reading a flat manifest causes no write and + no behavior change. +5. **Local paths stay local.** Project mappings never enter account sync, + telemetry, MCP responses, or logs intended for sharing. +6. **Deletion removes references, not knowledge.** Profile deletion cannot + delete user folders, git repositories, overlays, or retained Pack versions. +7. **No new resolution rule.** Profiles choose a layer set; the existing level + and recency rules resolve that set unchanged. +8. **The core stays dependency-free.** New core modules use Node.js built-ins. + +## 4. Manifest v2 Contract + +### 4.1 Canonical shape + +```json +{ + "profiles": { + "default": { + "label": "Default", + "layers": [ + { + "name": "personal-notes", + "level": 3, + "source": "files", + "path": "/Users/dana/Notes" + } + ] + }, + "context-cake": { + "label": "ContextCake", + "layers": [ + { + "name": "repo-docs", + "level": 3, + "source": "files", + "path": "/Users/dana/repos/context-cake" + }, + { + "name": "contextcake-pack", + "level": 0, + "source": "okf-local", + "path": "packs/contextcake/1.0.0", + "origin": "pack:contextcake@1.0.0" + } + ], + "pendingSources": [] + } + }, + "projects": { + "/Users/dana/repos/context-cake": "context-cake" + }, + "packs": {} +} +``` + +Rules: + +- A v2 manifest has `profiles` and no top-level `layers`. +- `default` always exists and cannot be deleted. +- Profile ids match `^[a-z0-9][a-z0-9-]{0,62}$`; labels are independent and may + change. +- Labels are Unicode-NFC, trimmed, 1–80 display characters, and reject control + characters, line breaks, and recognized credential patterns before they can + enter MCP instructions, UI, logs, or sync. +- `projects` keys are absolute local paths and values are profile ids. +- Each profile contains its own complete `layers` array. v1 does not add a + second global source registry or cross-profile layer references. +- Within a profile, the existing unique layer `name` is the v1 source identity; + sync and migration key it with profile id and source kind. Renaming is modeled + as remove/add and invalidates executable trust and cache identity. +- `pendingSources[]` contains synced source descriptors that are not runnable on + this Mac. Pending sources are visible repair tasks and are never passed to an + adapter factory, resolver, MCP server, sync operation, or live-layer lookup. +- `packs` remains a top-level retained-version registry; each assignment names + the profile that receives the Pack layer. +- A profile may contain at most one `live: true` layer, preserving team sync's + existing rule on a per-profile basis. + +### 4.2 Legacy compatibility and migration + +Reading a flat `{ "layers": [...] }` manifest returns an in-memory selection: + +```js +{ + mode: "legacy", + profile: { id: "default", label: "Default", layers: manifest.layers }, + reason: "legacy-default" +} +``` + +It does not write the manifest. Migration occurs only when the app needs to +persist more than one profile: + +1. Acquire the shared manifest mutation lock. +2. Re-read and validate the latest manifest. +3. Classify it as flat legacy or transitional hybrid. A transitional hybrid is + a currently shipped shape with top-level `layers` plus synced `profiles`, + `profilesOwnerUserId`, or `pendingSources`; it continues to run the flat + layers until this normalization. +4. Write an atomic mode-`0600` backup named with a UTC timestamp and SHA-256 of + the source manifest. Verify the completed backup and return its path/hash in + the migration result; never present an older unmatched backup as current. +5. Move the exact top-level `layers` array to `profiles.default.layers`. If a + synced `profiles.default` exists, merge descriptors by stable source identity: + locally runnable fields win and incomplete remote descriptors become pending + rather than overwriting local execution config. +6. Preserve every existing named profile. Move any layer that lacks its required + machine-local configuration or local executable trust into that profile's + `pendingSources`; preserve owner-binding metadata used by settings sync. +7. Move top-level `pendingSources` into `profiles.default.pendingSources`. +8. Change every Pack assignment with `profile: null` to + `profile: "default"`. +9. Add the new profile and any confirmed project mapping. +10. Validate the complete v2 candidate and every Pack/profile reference, write a + mode-`0600` temporary file, and + atomically rename it over the manifest. +11. Reload the service only after the rename succeeds. + +The migration is idempotent. A crash before manifest rename leaves the legacy or +transitional manifest; a crash after rename leaves the complete v2 manifest. +Backups are never read automatically. A recovery command verifies the backup's +content hash and warns that restoring it discards changes made after its +timestamp. + +### 4.3 Shared manifest module + +Create `packages/core/src/manifest.mjs` as the single boundary for: + +- parsing and structural validation; +- detecting legacy versus v2 mode; +- stable profile-id generation and validation; +- selecting one profile; +- listing profile summaries without opening sources; +- validating and canonicalizing project mappings; +- locked, atomic mutation and migration; +- safe layer-array access for Pack and source mutations. + +Validation returns three classes instead of one undifferentiated error: + +- **fatal structure:** unsafe keys, malformed profiles/layers, invalid ids, or a + canonical v2 document with two active schema roots; +- **inactive warning:** a stale mapping, pending source, or dangling reference in + an unselected profile; +- **selected fatal:** an explicit/matched profile, Pack assignment, or live-layer + reference that cannot be made safe. + +Move the Pack manager's private lock/write helpers into this module. Source +CRUD, profile CRUD, project mappings, and Pack operations must all use the same +lock to prevent lost updates between the app and concurrent CLI processes. + +## 5. Selection Algorithm + +Expose one pure selection function plus a thin filesystem canonicalization +step: + +```js +selectManifestProfile(manifest, { + requestedProfile, + cwd, + realpath, +}) -> { + mode, + profileId, + profileLabel, + layers, + reason, + matchedProjectRoot +} +``` + +Order: + +1. Validate the manifest without opening a source, separating fatal structure + from inactive warnings. +2. If flat legacy or transitional hybrid, return the top-level layers as the + virtual default stack. `--profile default` reports `reason: "explicit"`; + no override reports `legacy-default`; any other explicit id is an actionable + migration-required error. +3. If `requestedProfile` exists, require that exact profile and return + `reason: "explicit"`. +4. Canonicalize `cwd` and each existing mapping root with native `realpath`. +5. A root matches when `path.relative(root, cwd)` is empty or stays inside the + root by path segments. Never use `cwd.startsWith(root)`. +6. Sort matches by canonical root depth/length descending. The longest wins. +7. If equal-specificity canonical roots point to different profiles, return a + configuration error. +8. If the winning mapping names an unknown or invalid profile, fail closed. +9. Otherwise require and return `profiles.default` with + `reason: "default"`. + +Stale roots and dangling references in inactive profiles are returned as +warnings for the app and omitted from unrelated matching. A dangling reference +that wins selection is fatal. Paths are never emitted to shared logs or +telemetry. + +### Process binding + +- `contextcake mcp` selects once at startup from `--profile` and + `process.cwd()`. It never changes profile during the process lifetime. +- `resolver`, future profile-aware commands, and `contextcake profile current` + use the same selector. +- The desktop engine service selects from the app's persisted + `settings.activeProfile`; it does not use the app process working directory. +- `settings.activeProfile` is local-only. Settings sync may deliver profile + metadata but never an activation command or operative activity hint. +- A Console profile switch updates `settings.activeProfile`, tells the service + to replace its selected adapter set, and reloads all visible data. +- Existing agent processes are unaffected by an app switch. The UI explains + that a new agent session is required after mappings change. + +## 6. Core and MCP Changes + +### 6.1 Source construction + +- Change `buildSources()` to receive a selected profile/layer array, or add a + narrowly named `buildSelectedSources(selection, manifestDir, options)`. +- Prevent callers from passing a full v2 manifest without an explicit + selection. +- Derive a non-secret source fingerprint from profile id, source kind, + endpoint/repository or canonical local root, ref, content-path selection, and + adapter options. Use its hash—not raw paths—in every memory/disk/API-cache + namespace before the existing encoded display name. +- Move clone-backed GitHub checkouts from repository-slug-only paths to the same + profile/fingerprint namespace so the same repository at different refs cannot + share a mutable checkout. Explicit custom cache roots retain their location + contract but receive the fingerprint subdirectory. +- Preserve adapter behavior, numeric levels, and current warn-and-continue + semantics. + +### 6.2 Resolver and compatibility wrappers + +- Parse `--profile` in `resolver.mjs`. +- Select from the manifest before building sources. +- Keep root compatibility wrappers forwarding arguments unchanged. +- Update help and manifest documentation with selection order and examples. + +### 6.3 MCP server + +- Parse `--profile` without changing the existing boolean-flag behavior for + `--capture` and `--telemetry`. +- Use one selection object for source construction, live-layer discovery, + capture context, link traversal, and telemetry initialization. +- Make `resolveLiveLayer()` accept selected layers rather than search the full + manifest. +- Add the profile label, id, and non-sensitive selection reason to MCP + initialization instructions. Do not include a project path. +- Keep the four baseline read-tool schemas byte-for-byte stable and preserve the + two additional team-sync read tools. +- Keep capture opt-in. The selector must complete before the server exposes + write tools. +- Bind capture staging tokens to the selected profile id, manifest revision, and + live-layer identity. `confirm_capture` re-reads the manifest and rejects the + write if any binding changed or the profile was deleted. + +### 6.4 Promotion + +- Add a profile-aware promotion entry point that accepts `--manifest`, optional + `--profile`, the live capture id, and a target layer name from the selected + profile. +- Resolve source and destination roots from that one selection; do not accept + arbitrary filesystem roots in profile-aware mode. +- Retain the current raw-path promotion CLI only as a separately documented + legacy/advanced compatibility path. It carries no cross-profile isolation + claim. +- Revalidate profile id, manifest revision, live-layer identity, and target + layer before the approval write. + +### 6.5 Profile CLI + +Add `contextcake profile` with: + +- `current [--profile ] [--json]` — show the selected profile and reason; +- `list [--json]` — ids, labels, source count, mapping count, and validity; +- `create