diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index fa7d842..d1e2193 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -40,11 +40,11 @@ ### Client and Workspace Integration -- [ ] **CFG-01**: CAS generates profile-specific configuration for supported AI clients without overwriting unrelated user configuration. -- [ ] **CFG-02**: User can preview, validate, apply, and remove only CAS-owned client configuration. -- [ ] **CFG-03**: Profiles install and validate portable agent skills and workspace conventions from allowlisted sources. -- [ ] **CFG-04**: MCP configuration clearly distinguishes local workstation transports from production remote transports and never embeds secrets. -- [ ] **CFG-05**: Configuration adapters detect drift and preserve recoverable backups. +- [x] **CFG-01**: CAS generates profile-specific configuration for supported AI clients without overwriting unrelated user configuration. +- [x] **CFG-02**: User can preview, validate, apply, and remove only CAS-owned client configuration. +- [x] **CFG-03**: Profiles install and validate portable agent skills and workspace conventions from allowlisted sources. +- [x] **CFG-04**: MCP configuration clearly distinguishes local workstation transports from production remote transports and never embeds secrets. +- [x] **CFG-05**: Configuration adapters detect drift and preserve recoverable backups. ### Diagnostics and Support @@ -109,11 +109,11 @@ | OPS-05 | Phase 3 | Complete | | OPS-06 | Phase 3 | Complete | | OPS-07 | Phase 3 | Complete | -| CFG-01 | Phase 4 | Pending | -| CFG-02 | Phase 4 | Pending | -| CFG-03 | Phase 4 | Pending | -| CFG-04 | Phase 4 | Pending | -| CFG-05 | Phase 4 | Pending | +| CFG-01 | Phase 4 | Complete | +| CFG-02 | Phase 4 | Complete | +| CFG-03 | Phase 4 | Complete | +| CFG-04 | Phase 4 | Complete | +| CFG-05 | Phase 4 | Complete | | DIAG-01 | Phase 5 | Pending | | DIAG-02 | Phase 5 | Pending | | DIAG-03 | Phase 5 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2c2cf6c..2d835e5 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -11,7 +11,7 @@ CAS Workstation v1 progresses from a functional seed to a trustworthy desired-st | 1 | Governance and Quality Foundation | Establish enforceable contracts, test seams, CI, and traceability | GOV-01, GOV-02, GOV-03, GOV-04 | 4 | | 2 | Manifest, Inventory, and Safety Boundaries | Make desired state, ownership, paths, and destructive operations fail closed | MAN-01..05, SAFE-01..05 | 10 | | 3 | Transactional Plan and Apply Engine | Deliver idempotent setup, upgrade, repair, and recovery | OPS-01..07 | 7 | -| 4 | Client, Skills, and Workspace Profiles | Safely integrate supported AI clients and portable developer context | CFG-01..05 | 5 | +| 4 | Client, Skills, and Workspace Profiles | 3/3 | Complete | 2026-06-14 | | 5 | Diagnostics and Supportability | Make readiness, drift, failures, and support evidence actionable | DIAG-01..05 | 5 | | 6 | Trusted Release and Clean-Machine Proof | Produce verifiable releases and end-to-end operational evidence | REL-01, REL-02 | 2 | | 7 | Public Architecture and Operations Evidence | Complete the documentation and support surface required for adoption | REL-03, REL-04 | 2 | @@ -54,7 +54,7 @@ CAS Workstation v1 progresses from a functional seed to a trustworthy desired-st 3. Every operation is correlated and journaled with safe failure, resume, retry, or rollback guidance. 4. Dirty or diverged repositories and risky external operations fail closed by default. -### Phase 4: Client, Skills, and Workspace Profiles +### Phase 4: Client, Skills, and Workspace Profiles (Complete: 2026-06-14) **Goal:** Profiles safely install and maintain the AI-native context developers need without clobbering user configuration. diff --git a/.planning/STATE.md b/.planning/STATE.md index 15f3d20..ba1c2a4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,14 +2,14 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: ready_to_discuss -last_updated: "2026-06-12T17:45:00.000Z" +status: Phase 4 complete +last_updated: "2026-06-14T09:58:55.795Z" progress: total_phases: 7 - completed_phases: 3 - total_plans: 9 - completed_plans: 9 - percent: 43 + completed_phases: 4 + total_plans: 12 + completed_plans: 12 + percent: 57 --- # Project State @@ -23,7 +23,7 @@ See: `.planning/PROJECT.md` (updated 2026-06-11) ## Current Position -Phase: 4 +Phase: 4 — COMPLETE Plan: Not started - Project initialization: complete diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-01-PLAN.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-01-PLAN.md new file mode 100644 index 0000000..99aac6d --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-01-PLAN.md @@ -0,0 +1,43 @@ +--- +phase: 04-client-skills-and-workspace-profiles +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - stack.manifest.json + - schemas/manifest.schema.json + - schemas/operation-plan.schema.json + - schemas/managed-state.schema.json + - tests/fixtures/contracts/manifest.valid.json + - tests/Manifest.Tests.ps1 +requirements: [CFG-03, CFG-04] +requirements_addressed: [CFG-03, CFG-04] +autonomous: true +must_haves: + truths: + - "D-01 D-02: full remains the only golden-path profile and resource membership stays declarative" + - "D-06 D-08: skill/workspace sources and targets are allowlisted and unowned conflicts fail closed" + - "D-09 D-10: MCP transport intent is explicit and secret-bearing manifest fields are rejected" +--- + + +Strengthen declarative contracts and semantic policy for typed client, skill, workspace, and MCP resources. + + + +- T-04-01 (high): malicious manifest source, target, transport, or secret-bearing field reaches execution. Mitigate with schema and semantic deny-by-default validation before planning. +- T-04-03 (high): skill/workspace source escapes its allowlisted repository or target escapes an approved CAS boundary. Mitigate with relative-path contracts and canonical boundary validation. + + + + + Define safe Phase 4 manifest and operation contracts + stack.manifest.json, schemas/manifest.schema.json, schemas/operation-plan.schema.json, schemas/managed-state.schema.json, tests/fixtures/contracts/manifest.valid.json, tests/Manifest.Tests.ps1 + Extend client definitions with adapter, ownership key, and CAS-relative target metadata; extend skill/workspace definitions with allowlisted repository/source/target metadata; make MCP transport scope and authentication reference explicit without secret values; extend typed operation evidence where needed. Preserve the existing full profile and its four golden-path repos. + Invoke-Pester -Path tests/ContractSchemas.Tests.ps1,tests/Manifest.Tests.ps1 + + + +Invalid sources, targets, transports, secret-bearing fields, and unresolved references fail before planning; full remains the deterministic golden path. +Create `04-01-SUMMARY.md` after execution. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-01-SUMMARY.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-01-SUMMARY.md new file mode 100644 index 0000000..d66fc92 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-01-SUMMARY.md @@ -0,0 +1,7 @@ +# Phase 4 Plan 01 Summary + +Extended the manifest, schema, and semantic validator with allowlisted client +and managed-tree adapters, explicit MCP scope/auth-reference boundaries, safe +relative sources/targets, and preserved `full` as the declarative golden path. + +Verification: contract and manifest tests pass. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-02-PLAN.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-02-PLAN.md new file mode 100644 index 0000000..4888148 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-02-PLAN.md @@ -0,0 +1,47 @@ +--- +phase: 04-client-skills-and-workspace-profiles +plan: 02 +type: execute +wave: 2 +depends_on: ["04-01"] +files_modified: + - scripts/Cas.Workstation.psm1 + - tests/ClientConfig.Tests.ps1 + - tests/ManagedTrees.Tests.ps1 + - tests/Safety.Tests.ps1 +requirements: [CFG-01, CFG-02, CFG-03, CFG-05] +requirements_addressed: [CFG-01, CFG-02, CFG-03, CFG-05] +autonomous: true +must_haves: + truths: + - "D-03 D-04 D-05: client adapters surgically manage a namespaced owned subtree with atomic backup and owned-content digest" + - "D-06 D-07 D-08: skills/workspaces are deterministic managed trees and unowned conflicts fail closed" + - "D-12: inventory distinguishes satisfied, missing, drifted, conflicting, and unsupported states" +--- + + +Implement reusable owned-subtree client adapters and deterministic managed-tree adapters with drift evidence. + + + +- T-04-02 (high): client merge or uninstall overwrites unrelated user settings. Mitigate with namespaced surgical merge/removal, canonical owned-node digest, validation, and atomic backup. +- T-04-03 (high): managed tree follows reparse points or deletes unexpected content. Mitigate with staged deterministic entries, safe-path checks, and per-entry ownership. + + + + + Implement namespaced client configuration adapters + scripts/Cas.Workstation.psm1, tests/ClientConfig.Tests.ps1, tests/Safety.Tests.ps1 + Add pure desired-fragment rendering, inspection, merge, digest, apply, verification, and surgical removal helpers. Preserve unrelated JSON settings, reject malformed/conflicting targets, and use atomic backup/write primitives. + Invoke-Pester -Path tests/ClientConfig.Tests.ps1,tests/Safety.Tests.ps1 + + + Implement deterministic skill and workspace tree adapters + scripts/Cas.Workstation.psm1, tests/ManagedTrees.Tests.ps1, tests/Safety.Tests.ps1 + Add allowlisted source/target resolution, deterministic per-file tree digests, staged copy, conflict detection, safe apply, and owned-tree verification without recursive adoption. + Invoke-Pester -Path tests/ManagedTrees.Tests.ps1,tests/Safety.Tests.ps1 + + + +Adapters preserve unrelated state, produce deterministic owned-content evidence, and fail closed for malformed, escaping, reparse-point, or unowned-conflict inputs. +Create `04-02-SUMMARY.md` after execution. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-02-SUMMARY.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-02-SUMMARY.md new file mode 100644 index 0000000..c6edaae --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-02-SUMMARY.md @@ -0,0 +1,7 @@ +# Phase 4 Plan 02 Summary + +Implemented namespaced client merge/removal, owned-content drift digests, +atomic backup/write behavior, deterministic managed-tree digests, safe copies, +and unowned-target conflict handling. + +Verification: client, managed-tree, and safety tests pass. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-03-PLAN.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-03-PLAN.md new file mode 100644 index 0000000..5725191 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-03-PLAN.md @@ -0,0 +1,54 @@ +--- +phase: 04-client-skills-and-workspace-profiles +plan: 03 +type: execute +wave: 3 +depends_on: ["04-01", "04-02"] +files_modified: + - scripts/Cas.Workstation.psm1 + - tests/Plan.Tests.ps1 + - tests/Apply.Tests.ps1 + - tests/OperationWorkflow.Tests.ps1 + - tests/Uninstall.Tests.ps1 + - README.md + - docs/traceability.json + - .planning/REQUIREMENTS.md + - .planning/ROADMAP.md + - .planning/STATE.md +requirements: [CFG-01, CFG-02, CFG-03, CFG-04, CFG-05] +requirements_addressed: [CFG-01, CFG-02, CFG-03, CFG-04, CFG-05] +autonomous: true +must_haves: + truths: + - "D-11: all client, skill, and workspace mutation executes only through typed deterministic journaled operations" + - "D-12: repair reconciles only CAS-owned drift and satisfied resources become skips" + - "D-03 D-05: uninstall removes only the CAS-owned client subtree and never restores a whole stale user file" + - "D-01: public documentation continues to identify full as the verified golden-path profile" +--- + + +Integrate Phase 4 adapters with inventory, deterministic planning, journaled apply, ledger-only uninstall, and public evidence. + + + +- T-04-04 (high): direct helper mutation bypasses preview, plan integrity, journal, or ownership evidence. Mitigate by routing typed operations only through the existing operation engine. +- T-04-02 (high): uninstall restores stale whole-file backup over later user changes. Mitigate with specialized surgical client removal. + + + + + Integrate typed inventory, planning, apply, ownership, and uninstall + scripts/Cas.Workstation.psm1, tests/Plan.Tests.ps1, tests/Apply.Tests.ps1, tests/OperationWorkflow.Tests.ps1, tests/Uninstall.Tests.ps1 + Add deterministic dependency-aware operations for selected clients, skills, and workspaces; classify drift; execute adapters through the journaled engine; persist managed ownership/digests; and surgically uninstall only owned configuration/tree entries. + Invoke-Pester -Path tests/Plan.Tests.ps1,tests/Apply.Tests.ps1,tests/OperationWorkflow.Tests.ps1,tests/Uninstall.Tests.ps1 + + + Complete Phase 4 documentation and traceability + README.md, docs/traceability.json, .planning/REQUIREMENTS.md, .planning/ROADMAP.md, .planning/STATE.md + Document profile-specific client/skill/workspace preview, apply, repair, MCP security boundaries, and surgical uninstall. Map CFG-01 through CFG-05 to executable evidence and mark Phase 4 complete only after the full quality gate passes. + powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-Quality.ps1 + + + +Selected Phase 4 resources are previewable, idempotent, journaled, repairable, and removable only within proven CAS ownership while unrelated user state survives. +Create `04-03-SUMMARY.md` and `04-VERIFICATION.md` after execution. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-03-SUMMARY.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-03-SUMMARY.md new file mode 100644 index 0000000..e55969b --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-03-SUMMARY.md @@ -0,0 +1,8 @@ +# Phase 4 Plan 03 Summary + +Integrated selected clients, skills, and workspaces into deterministic +dependency-aware planning, journaled apply, ownership evidence, repair +classification, and surgical ledger-only uninstall. + +Verification: full quality gate passes with 59 tests, including review-driven +clean-bootstrap and digest-proven managed-tree lifecycle coverage. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-CONTEXT.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-CONTEXT.md new file mode 100644 index 0000000..fd4c953 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-CONTEXT.md @@ -0,0 +1,155 @@ +# Phase 4: Client, Skills, and Workspace Profiles - Context + +**Gathered:** 2026-06-13 +**Status:** Ready for planning + + +## Phase Boundary + +Phase 4 makes declaratively selected AI client configuration, portable skills, +and workspace conventions first-class resources in the existing preview-first, +journaled plan/apply engine. It must detect and repair drift while preserving +unrelated user state. Broader diagnostics, release automation, and fleet +management remain later-phase work. + + + + +## Implementation Decisions + +### Golden-Path Profile +- **D-01:** Preserve `full` as the declarative golden-path profile; do not add a + duplicate `golden-path` profile name. It must continue to select + `cas-platform`, `cas-contracts`, `cas-evals`, and `cas-reference-product`. +- **D-02:** Client, skill, and workspace operations are derived only from the + selected profile and manifest catalogs. No adapter may hard-code profile + membership. + +### Client Configuration Ownership +- **D-03:** Each supported client uses an explicit adapter that previews, + validates, backs up, atomically applies, verifies, and surgically removes + only CAS-owned configuration. +- **D-04:** CAS-owned client configuration is namespaced and carries stable + ownership and content-digest evidence. Unrelated keys and user changes are + preserved during apply, repair, and removal. +- **D-05:** Existing user-owned targets require a recoverable backup before + first modification. Backups are recovery evidence, not permission to replace + later unrelated user changes during uninstall. + +### Skills and Workspaces +- **D-06:** Skills and workspace conventions are installed only from + allowlisted manifest sources into approved CAS-managed boundaries. +- **D-07:** Skill and workspace resources receive the same deterministic plan, + ownership-ledger, drift-digest, safe-path, and uninstall-only-owned behavior + as other managed resources. +- **D-08:** Existing unowned skill or workspace targets fail closed on conflict; + CAS does not silently adopt or overwrite them. + +### MCP Security Boundary +- **D-09:** MCP configuration explicitly labels local workstation transports + (`stdio`) separately from production remote transports (`http` or `sse`). +- **D-10:** Generated configuration may contain non-secret authentication + references or instructions, but never credentials, access tokens, API keys, + or embedded secrets. + +### Planning, Drift, and Recovery +- **D-11:** Clients, skills, and workspaces become typed operations in + `New-CasOperationPlan` and execute through `Invoke-CasOperationPlan`; direct + mutation helpers must not bypass preview, journal, or correlation evidence. +- **D-12:** Inventory compares canonical managed content digests and reports + satisfied, missing, drifted, conflicting, or unsupported state. Repair + reconciles only CAS-owned drift. + +### the agent's Discretion +- Exact adapter function decomposition, namespaced JSON shape, and manifest + property names, provided they remain PowerShell 5.1-compatible, + deterministic, schema-validated, and fail closed. +- Whether the first implementation plan separates client adapters from + skill/workspace adapters, provided all CFG-01 through CFG-05 requirements are + covered by the completed phase. + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Product and Requirements +- `.planning/ROADMAP.md` - Phase 4 goal and success criteria. +- `.planning/REQUIREMENTS.md` - CFG-01 through CFG-05. +- `.planning/PROJECT.md` - Configuration, safety, state, and authentication constraints. +- `AGENTS.md` - Repository engineering rules and mandatory GSD workflow. + +### Prior Decisions and Current Contracts +- `.planning/phases/02-manifest-inventory-and-safety-boundaries/02-CONTEXT.md` - Fail-closed manifest, ownership, path, backup, and uninstall decisions. +- `.planning/phases/03-transactional-plan-and-apply-engine/03-CONTEXT.md` - Deterministic plan/apply, journal, resume, and repair decisions. +- `stack.manifest.json` - Current profiles, client, skill, workspace, and MCP declarations. +- `schemas/manifest.schema.json` - Manifest contract to extend safely. +- `schemas/managed-state.schema.json` - Ownership and digest evidence contract. +- `schemas/operation-plan.schema.json` - Typed operation-plan contract. + +### Existing Implementation and Verification +- `scripts/Cas.Workstation.psm1` - Desired-state resolution, safety, atomic writes, ownership, planner, apply engine, and legacy client-fragment generator. +- `tests/Manifest.Tests.ps1` - Declarative resolution and allowlist tests. +- `tests/Plan.Tests.ps1` - Deterministic planning and idempotency tests. +- `tests/Apply.Tests.ps1` - Journaled apply and failure behavior tests. +- `tests/Safety.Tests.ps1` - Atomic backup and ownership-ledger tests. +- `tests/Uninstall.Tests.ps1` - Ledger-only removal and restoration tests. + + + + +## Existing Code Insights + +### Reusable Assets +- `Resolve-CasDesiredState` already resolves clients, skills, and workspaces + deterministically from profile declarations. +- `Write-CasAtomicJson`, `Add-CasManagedResource`, and + `Write-CasManagedState` provide backup, ownership, and atomic-write + foundations. +- `New-CasOperationPlan` and `Invoke-CasOperationPlan` provide deterministic + preview and journaled execution extension points. +- `New-CasClientConfigs` provides legacy isolated fragment behavior that can be + replaced or wrapped by typed client adapters. + +### Established Patterns +- Schema plus semantic validation occurs before external execution. +- All mutation targets are revalidated against approved boundaries. +- Satisfied resources produce deterministic `skip` operations. +- Uninstall acts only on current ownership-ledger evidence. + +### Integration Points +- Extend manifest client, skill, workspace, and MCP definitions without + hard-coding selected resources. +- Extend inventory and planner switches for client, skill, and workspace + operation kinds. +- Register ownership and content digests after successful adapter execution. +- Keep `setup.ps1`, `upgrade.ps1`, and `repair.ps1` as thin entry points over + the shared operation engine. + + + + +## Specific Ideas + +- The public portfolio golden path remains the existing `full` profile and the + four already-declared CAS repositories. +- Local `stdio` MCP is valid for workstation integration but must not be + presented as the production remote architecture. + + + + +## Deferred Ideas + +- Rich doctor findings and support bundles belong to Phase 5. +- Clean-machine end-to-end proof and trusted release artifacts belong to Phase 6. +- Central profile distribution and fleet management remain v2 (`PLAT-03`). + + + +--- + +*Phase: 04-client-skills-and-workspace-profiles* +*Context gathered: 2026-06-13* diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-DISCUSSION-LOG.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-DISCUSSION-LOG.md new file mode 100644 index 0000000..69da763 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-DISCUSSION-LOG.md @@ -0,0 +1,77 @@ +# Phase 4: Client, Skills, and Workspace Profiles - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution +> agents. Decisions are captured in CONTEXT.md. + +**Date:** 2026-06-13 +**Phase:** 04-client-skills-and-workspace-profiles +**Mode:** `$gsd-discuss-phase 4 --auto` +**Areas discussed:** golden-path profile, client ownership, skills and +workspaces, MCP boundaries, drift and recovery + +--- + +## Golden-Path Profile + +| Option | Description | Selected | +|--------|-------------|----------| +| Preserve `full` | Keep the existing profile as the single declarative golden path | Yes | +| Add `golden-path` | Add a duplicate profile selecting the same repositories | No | + +**Auto-selected choice:** Preserve `full`. +**Notes:** The current manifest and README already establish `full` as the +golden path and include the four cross-repository proof components. + +## Client Configuration Ownership + +| Option | Description | Selected | +|--------|-------------|----------| +| Namespaced surgical merge | Preserve unrelated settings and remove only CAS-owned entries | Yes | +| Replace complete client files | Treat the whole client file as CAS-owned | No | +| Isolated fragments only | Generate fragments without supported merge/apply behavior | No | + +**Auto-selected choice:** Namespaced surgical merge. +**Notes:** Existing user files require backups, atomic writes, and stable +ownership/content-digest evidence. + +## Skills and Workspaces + +| Option | Description | Selected | +|--------|-------------|----------| +| First-class managed resources | Plan, journal, validate, repair, and uninstall only owned resources | Yes | +| Best-effort copy helpers | Copy selected content outside the plan/apply engine | No | + +**Auto-selected choice:** First-class managed resources. +**Notes:** Conflicting unowned targets fail closed. + +## MCP Boundaries + +| Option | Description | Selected | +|--------|-------------|----------| +| Explicit transport and auth boundary | Label local versus remote transports and prohibit embedded secrets | Yes | +| Uniform opaque server entries | Treat all MCP transports and authentication alike | No | + +**Auto-selected choice:** Explicit transport and auth boundary. +**Notes:** Local workstation `stdio` remains valid, while production remote MCP +must use an external secure identity model. + +## Drift and Recovery + +| Option | Description | Selected | +|--------|-------------|----------| +| Canonical digest plus owned repair | Detect drift and reconcile only CAS-owned content | Yes | +| Overwrite desired files on repair | Replace complete files whenever drift exists | No | + +**Auto-selected choice:** Canonical digest plus owned repair. +**Notes:** All operations remain preview-first and journaled. + +## the agent's Discretion + +- Exact adapter decomposition and manifest property names. +- Plan split, provided all Phase 4 requirements are covered. + +## Deferred Ideas + +- Phase 5 diagnostics and support bundles. +- Phase 6 clean-machine and trusted-release evidence. +- v2 centrally governed workstation fleets. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-PATTERNS.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-PATTERNS.md new file mode 100644 index 0000000..ef82d3f --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-PATTERNS.md @@ -0,0 +1,490 @@ +# Phase 4: Client, Skills, and Workspace Profiles - Pattern Map + +**Mapped:** 2026-06-13 +**Files analyzed:** 11 implementation/contract/test files +**Analogs found:** 8 / 8 behavior areas + +## Scope Summary + +Phase 4 should extend the existing single-module, manifest-driven operation engine. +The closest patterns live in `scripts/Cas.Workstation.psm1`; no separate adapter +directory currently exists. Keep direct mutation behind typed operations and use +the existing safety, ownership, journal, and uninstall functions. + +The legacy `New-CasClientConfigs` function is payload-shape evidence only. Its +direct directory creation and `Set-Content` calls bypass safe-path validation, +atomic backup/write, ownership evidence, planning, and journaling, so new client +adapters must not copy its mutation behavior. + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|---|---|---|---|---| +| `scripts/Cas.Workstation.psm1` | service, adapter, utility | transform, file-I/O, request-response | Existing desired-state, safety, planner/apply, and uninstall functions in the same module | exact extension point | +| `stack.manifest.json` | config | declarative transform | Existing `clients`, `skills`, `workspaces`, `sharedMcpServer`, and `full` profile declarations | exact extension point | +| `schemas/manifest.schema.json` | config/contract | validation | Existing `$defs.client`, `$defs.workspace`, profile selections, policy, and MCP contract | exact extension point | +| `schemas/managed-state.schema.json` | model/contract | CRUD, file-I/O | Existing resource ownership and content-digest contract | exact extension point | +| `schemas/operation-plan.schema.json` | model/contract | transform, request-response | Existing typed operation object and enums | exact extension point | +| `tests/Manifest.Tests.ps1` | test | validation, transform | Existing declarative resolution, allowlist, and deterministic digest tests | exact | +| `tests/Plan.Tests.ps1` and `tests/Apply.Tests.ps1` | test | request-response, event-driven | Existing deterministic skip/update plan and journaled apply tests | exact | +| `tests/Safety.Tests.ps1` and `tests/Uninstall.Tests.ps1` | test | file-I/O, CRUD | Existing atomic backup, ownership, safe-path, and ledger-only removal tests | exact | + +## Pattern Assignments + +### Client Configuration Merge and Drift + +**Implementation location:** `scripts/Cas.Workstation.psm1` + +**Closest reusable functions:** + +- `Resolve-CasDesiredState` (`169-207`) selects clients only from the chosen + profile and manifest catalog and creates the deterministic desired-state + digest. +- `ConvertTo-CasCanonicalJson` and `Get-CasSha256` (`150-167`) provide stable + content-digest evidence for the CAS-owned namespace. +- `Write-CasAtomicJson` (`407-442`) provides validated JSON serialization, + recoverable first backup, atomic replace/move, and temporary-file cleanup. +- `Add-CasManagedResource` (`354-386`) records ownership, backup target, and + content digest. +- `Get-CasOperationInventory` (`989-1012`) is the inventory extension point for + returning `satisfied`, `missing`, `drifted`, `conflicting`, or `unsupported` + client states. + +**Desired-state selection pattern** (`scripts/Cas.Workstation.psm1:185-206`): + +```powershell +foreach ($category in @("tools", "repos", "services", "clients", "skills", "workspaces")) { + $catalog = @($Manifest.$category) + foreach ($required in @($true, $false)) { + $level = if ($required) { "required" } else { "optional" } + foreach ($id in @($profileDefinition.$category.$level | Sort-Object)) { + $definition = $catalog | Where-Object id -eq $id | Select-Object -First 1 + $resolved.resources += [ordered]@{ + category = $category + id = $id + required = $required + definition = ConvertTo-CasCanonicalValue -Value $definition + } + } + } +} +$canonical = ConvertTo-CasCanonicalJson -InputObject $resolved +``` + +**Atomic merge-write boundary** (`scripts/Cas.Workstation.psm1:415-441`): + +```powershell +$target = Assert-CasSafePath -Path $Path -ApprovedRoots $ApprovedRoots -AllowBoundary:$AllowBoundary +$json = $InputObject | ConvertTo-Json -Depth 30 +$null = $json | ConvertFrom-Json +$temp = Join-Path $directory ".$([IO.Path]::GetFileName($target)).$([Guid]::NewGuid().ToString('N')).tmp" +[IO.File]::WriteAllText($temp, $json, (New-Object Text.UTF8Encoding($false))) +$null = Get-Content -LiteralPath $temp -Raw | ConvertFrom-Json +if (Test-Path -LiteralPath $target -PathType Leaf) { + $backup = "$target.backup.$([DateTime]::UtcNow.ToString('yyyyMMddHHmmssfff'))" + [IO.File]::Replace($temp, $target, $backup) +} +``` + +**Legacy payload shape, not mutation pattern** +(`scripts/Cas.Workstation.psm1:1533-1546`): + +```powershell +$sharedServer = [ordered]@{ + mcpServers = @{ + ($Manifest.sharedMcpServer.name) = @{ + command = $Manifest.sharedMcpServer.command + args = @($promptImproverEntry) + transport = $Manifest.sharedMcpServer.transport + } + } +} +``` + +New adapters should read existing client JSON, validate it, surgically replace +only a stable CAS-owned namespace, preserve all unrelated keys, calculate a +canonical digest for the owned fragment, then call `Write-CasAtomicJson`. +Existing unowned conflicting namespaces must fail closed. + +**Closest tests:** + +- `tests/Safety.Tests.ps1:70-81`: existing valid target is backed up before + atomic replacement. +- `tests/Manifest.Tests.ps1:34-48`: declarative category resolution and stable + digest. +- `tests/Plan.Tests.ps1:28-35`: satisfied state becomes `skip`. +- `tests/RepositorySafety.Tests.ps1:13-18`: closest fail-closed drift/conflict + evidence pattern. +- `tests/Uninstall.Tests.ps1:79-91`: closest modified-configuration restoration + test, but Phase 4 must add a surgical removal test that preserves user changes + made after the initial backup. + +**No exact analog:** There is no existing surgical JSON merge/removal function. +Do not model uninstall as unconditional full-backup restoration when unrelated +user keys may have changed after CAS apply. + +--- + +### Atomic Backup and Write + +**Analog:** `Write-CasAtomicJson`, `Write-CasManagedState`, +`Restore-CasBackupAtomically` + +**Write wrapper pattern** (`scripts/Cas.Workstation.psm1:444-453`): + +```powershell +Assert-CasManagedState -State $State +Write-CasAtomicJson -InputObject $State -Path $Path -ApprovedRoots $ApprovedRoots +``` + +**Atomic restoration pattern** (`scripts/Cas.Workstation.psm1:539-558`): + +```powershell +Copy-Item -LiteralPath $BackupPath -Destination $temp -Force +if (Test-Path -LiteralPath $TargetPath -PathType Leaf) { + [IO.File]::Replace($temp, $TargetPath, $replacedBackup) +} +else { + [IO.File]::Move($temp, $TargetPath) +} +``` + +Apply this pattern to client JSON and generated CAS-owned files. Validate target +boundaries before every write, validate serialized content before replacement, +and retain backup evidence in managed state. + +**Closest tests:** + +- `tests/Safety.Tests.ps1:55-68`: managed state validates and leaves no temp. +- `tests/Safety.Tests.ps1:70-81`: backup contains old content and target contains + new content. +- `tests/Uninstall.Tests.ps1:79-91`: atomic backup restoration. + +--- + +### Ownership Ledger + +**Analog:** `New-CasManagedState`, `Add-CasManagedResource`, +`Assert-CasManagedState`, `Write-CasManagedState` + +**Ownership registration pattern** (`scripts/Cas.Workstation.psm1:366-384`): + +```powershell +if ($Ownership -eq "created" -and $WasPresentBefore) { + throw "Resource '$Id' cannot be owned as created because it existed before CAS management." +} +if ($Ownership -eq "modified" -and (-not $WasPresentBefore -or -not $BackupTarget)) { + throw "Modified resource '$Id' requires pre-existing evidence and a backup target." +} +$State.resources += [pscustomobject]@{ + id = $Id + kind = $Kind + ownership = $Ownership + target = Resolve-CasCanonicalPath -Path $Target + backupTarget = if ($BackupTarget) { Resolve-CasCanonicalPath -Path $BackupTarget } else { $null } + contentDigest = if ($ContentDigest) { $ContentDigest } else { $null } +} +``` + +Use stable IDs such as `client:`, `skill:`, and `workspace:`. Record +`created` only for absent targets, `modified` only with backup evidence, and +never adopt pre-existing unowned skill/workspace targets. + +**Contract analog** (`schemas/managed-state.schema.json:18-30`): + +- Existing kinds include `directory`, `file`, `repository`, `configuration`, + and `tool`. +- Ownership is `created`, `modified`, or `observed`. +- `contentDigest` already supports canonical drift evidence. +- Modified resources require `backupTarget` and `wasPresentBefore: true`. + +**Closest tests:** + +- `tests/Safety.Tests.ps1:43-53`: rejects claiming pre-existing resources and + requires backup evidence. +- `tests/Safety.Tests.ps1:55-68`: persists ledger atomically. + +--- + +### Operation Planning and Apply + +**Analog:** `New-CasOperationPlan`, `Assert-CasOperationPlan`, +`Invoke-CasPlannedOperation`, `Invoke-CasOperationPlan` + +**Planner switch pattern** (`scripts/Cas.Workstation.psm1:1030-1066`): + +```powershell +foreach ($resource in @($resolved.desiredState.resources | Sort-Object category, id)) { + $inventoryId = "$($resource.category.TrimEnd('s')):$($resource.id)" + $actual = @($Inventory.resources | Where-Object id -eq $inventoryId | Select-Object -First 1) + switch ($resource.category) { + # Add clients, skills, and workspaces beside tools and repos. + } +} +``` + +**Deterministic identity pattern** (`scripts/Cas.Workstation.psm1:1069-1089`): + +```powershell +$sortedOperations = @($operations.ToArray() | Sort-Object { $_.id }) +$identity = [ordered]@{ + schemaVersion = "1.0.0" + mode = $Mode + profile = $Profile + rootPath = Resolve-CasCanonicalPath -Path $RootPath + configPath = Resolve-CasCanonicalPath -Path $ConfigPath + desiredStateDigest = $resolved.digest + operations = $sortedOperations +} +$planId = Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject $identity) +``` + +**Executor dispatch pattern** (`scripts/Cas.Workstation.psm1:1218-1247`): + +```powershell +if ($Operation.action -eq "skip") { + return +} +if ($Operation.kind -eq "tool") { + # adapter execution + return +} +if ($Operation.kind -eq "repository") { + # adapter execution + return +} +throw "No executor is registered for operation kind '$($Operation.kind)'." +``` + +Add explicit client/configuration, skill, and workspace executors here or +dispatch to focused adapter functions from here. Direct helpers must not mutate +outside this call path. + +**Journal/apply pattern** (`scripts/Cas.Workstation.psm1:1289-1328`): + +- Skip operations are journaled and emit correlated skipped events. +- Before each attempt, journal status is atomically persisted. +- Adapter exceptions become failed events and actionable resume guidance. +- A failed operation stops later operations. + +**Closest tests:** + +- `tests/Plan.Tests.ps1:9-17`: deterministic plan ID and operation ordering. +- `tests/Plan.Tests.ps1:19-35`: preview evidence and idempotent skips. +- `tests/Apply.Tests.ps1:34-70`: correlated success/skip, bounded failure, + resume, and tamper rejection. +- `tests/OperationWorkflow.Tests.ps1:11-22`: shared setup/upgrade/repair planner + and mutation-free preview. + +--- + +### Skill and Workspace Directories + +**Closest analogs:** `Resolve-CasDesiredState`, `Assert-CasSafePath`, +`New-CasDirectoryLayout`, and repository create/update planning + +**Directory layout evidence** (`scripts/Cas.Workstation.psm1:635-651`): + +```powershell +$paths = @( + $RootPath, + (Join-Path $RootPath $Manifest.paths.reposRoot), + $ConfigPath, + (Join-Path $ConfigPath $Manifest.paths.state), + (Join-Path $ConfigPath $Manifest.paths.config) +) +foreach ($path in $paths) { + if (-not (Test-Path -LiteralPath $path)) { + New-Item -ItemType Directory -Path $path -Force | Out-Null + } +} +``` + +Use this only as path-composition evidence. Phase 4 skill/workspace creation +must be planned and applied through typed operations, call `Assert-CasSafePath` +before mutation, and fail if an unowned target already exists. + +**Safe-path guard pattern** (`scripts/Cas.Workstation.psm1:302-334`): + +- Canonicalize target and approved roots. +- Require target inside an approved boundary. +- Reject forbidden roots/system directories. +- Reject existing reparse-point targets or ancestors. + +**Closest tests:** + +- `tests/Manifest.Tests.ps1:34-39`: skills/workspaces resolve from profile data. +- `tests/Safety.Tests.ps1:7-39`: canonical child acceptance, boundary escape, + boundary root, and reparse-point rejection. +- `tests/Uninstall.Tests.ps1:56-66`: refuses removal of an owned directory that + contains unexpected state. + +**No exact analog:** There is no current skill/workspace copy or validation +adapter. The planner should require tests for allowlisted source resolution, +created-target ownership, unowned-target conflict, digest drift, idempotent +skip, and non-recursive safe uninstall. + +--- + +### Drift Detection + +**Analog:** `Get-CasOperationInventory` plus `New-CasOperationPlan` + +**Inventory pattern** (`scripts/Cas.Workstation.psm1:996-1011`): + +```powershell +$resources = New-Object System.Collections.Generic.List[object] +# Inspect each selected resource and emit stable id/status/detail evidence. +[pscustomobject]@{ resources = $resources.ToArray() } +``` + +For clients, skills, and workspaces, compare the canonical digest of current +CAS-owned content against ledger `contentDigest`. Return explicit states: +`satisfied`, `missing`, `drifted`, `conflicting`, or `unsupported`. Planner +logic should map satisfied to `skip`, owned missing/drifted to create/update, +and unowned conflict/unsupported to a fail-closed operation or planning error. + +**Closest tests:** + +- `tests/Plan.Tests.ps1:28-43`: inventory status drives skip versus update. +- `tests/RepositorySafety.Tests.ps1:8-18`: pure evidence conversion with + fail-closed conflict branches. +- `tests/Manifest.Tests.ps1:41-48`: canonical digest determinism. + +--- + +### Uninstall + +**Analog:** `Get-CasUninstallPreview`, `Invoke-CasUninstall` + +**Preview pattern** (`scripts/Cas.Workstation.psm1:486-523`): + +```powershell +foreach ($resource in @($state.resources)) { + if ($resource.ownership -eq "observed") { + # preserve + continue + } + $target = Assert-CasSafePath -Path $resource.target -ApprovedRoots $ApprovedRoots -AllowBoundary + # modified -> restore-backup; created -> remove-created +} +``` + +**Apply pattern** (`scripts/Cas.Workstation.psm1:568-594`): + +```powershell +$actions = @($Preview.actions | Where-Object actionable | Sort-Object { $_.target.Length } -Descending) +foreach ($action in $actions) { + $target = Assert-CasSafePath -Path $action.target -ApprovedRoots $ApprovedRoots -AllowBoundary + if (-not $PSCmdlet.ShouldProcess($target, $action.action)) { + continue + } + # Apply only previewed ledger-backed action. +} +``` + +Client configuration needs a specialized uninstall action that removes only the +CAS-owned namespace from the current file. Full backup restoration is correct +only when it cannot erase later unrelated user changes. Skills/workspaces must +remove only ledger-created targets and must refuse recursive removal when +unexpected content exists. + +**Closest tests:** + +- `tests/Uninstall.Tests.ps1:16-26`: preview does not mutate. +- `tests/Uninstall.Tests.ps1:28-54`: observed/unrelated state is preserved. +- `tests/Uninstall.Tests.ps1:56-77`: unexpected directory content and unsafe + ledger paths fail closed. +- `tests/Uninstall.Tests.ps1:79-91`: modified file backup restoration baseline. + +## Contract Patterns + +### Manifest + +**Sources:** `stack.manifest.json:10-35,328-361`, +`schemas/manifest.schema.json:7-33` + +- Keep `full` as the golden-path profile; its profile membership remains + declarative. +- Extend catalogs and policy fields rather than hard-coding adapter membership. +- Existing semantic validation in `Assert-CasManifest` + (`scripts/Cas.Workstation.psm1:46-121`) validates required properties, + uniqueness, allowlisted commands/repos/config targets, and profile references + before execution. +- MCP transport enum already distinguishes `stdio`, `http`, and `sse`. +- Add semantic validation that rejects secret-bearing MCP/client fields and + validates skill/workspace allowlisted sources and approved target metadata. + +### Managed State + +**Source:** `schemas/managed-state.schema.json:7-34` + +Reuse the existing ownership and digest shape. Extend resource kinds only if +typed `skill` and `workspace` kinds add planner/uninstall clarity; otherwise use +`directory`, `file`, and `configuration` consistently with stable IDs. + +### Operation Plan + +**Source:** `schemas/operation-plan.schema.json:7-17` + +Extend the operation `kind` enum and any required adapter metadata in lockstep +with `Assert-CasOperationPlan`, planner output, executor dispatch, and positive/ +negative fixtures. Preserve `additionalProperties: false`. + +## Shared Patterns + +### Fail Closed Before Mutation + +**Sources:** `Get-CasManifest` (`scripts/Cas.Workstation.psm1:11-29`), +`Assert-CasManifest` (`46-121`), `Assert-CasSafePath` (`295-335`), +`Assert-CasOperationPlan` (`1093-1124`) + +Validate JSON, semantic allowlists, canonical paths, ownership evidence, and +plan integrity before any adapter performs file I/O. + +### Canonical Digests + +**Source:** `scripts/Cas.Workstation.psm1:123-167` + +Use `ConvertTo-CasCanonicalJson` followed by `Get-CasSha256`; do not hash +formatting-dependent raw JSON. + +### Preview First and Journaled Apply + +**Sources:** `Invoke-CasWorkstationOperation` +(`scripts/Cas.Workstation.psm1:1394-1440`) and `Invoke-CasOperationPlan` +(`1250-1337`) + +Preview returns the deterministic plan without mutation. Apply and resume route +through the same engine, with atomic journal updates and correlated events. + +### Test Structure + +**Sources:** all existing Pester files + +- Import the shared module in `BeforeAll`. +- Use `$TestDrive` for isolated filesystem behavior. +- Construct explicit inventory/state objects instead of invoking real external + tools. +- Test both success and failure paths with `Should -Throw`. +- For contracts, update both positive and negative fixtures; the generic + coverage test is `tests/ContractSchemas.Tests.ps1:16-22`. + +## No Exact Analog Found + +| Behavior | Closest Existing Pattern | Planner Guidance | +|---|---|---| +| Surgical merge into a user-owned client JSON file | `Write-CasAtomicJson` plus legacy `New-CasClientConfigs` payload | Add focused adapter/helper and tests preserving unrelated keys | +| Surgical client uninstall after later user edits | `Get-CasUninstallPreview` / `Invoke-CasUninstall` | Remove only current CAS-owned namespace; do not blindly restore whole backup | +| Skill installation/validation adapter | Repository planning plus safe directory ownership | Add allowlisted source/target adapter and conflict tests | +| Workspace convention installation/validation adapter | Directory layout plus safe path and ledger patterns | Add planned file/directory operations and digest validation | +| Client/skill/workspace drift inventory | Repository inventory/status pattern | Add canonical owned-content digest comparison and explicit status mapping | + +## Metadata + +**Analog search scope:** `scripts/`, `schemas/`, `tests/`, `stack.manifest.json`, +Phase 2/3/4 context, roadmap, requirements, and project constraints + +**Primary module scanned:** `scripts/Cas.Workstation.psm1` (1,587 lines) + +**Pattern extraction date:** 2026-06-13 diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-RESEARCH.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-RESEARCH.md new file mode 100644 index 0000000..042813a --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-RESEARCH.md @@ -0,0 +1,715 @@ +# Phase 4: Client, Skills, and Workspace Profiles - Research + +**Researched:** 2026-06-13 +**Domain:** Safe PowerShell 5.1 configuration adapters and managed developer-context resources +**Confidence:** HIGH + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +### Golden-Path Profile +- **D-01:** Preserve `full` as the declarative golden-path profile; do not add a + duplicate `golden-path` profile name. It must continue to select + `cas-platform`, `cas-contracts`, `cas-evals`, and `cas-reference-product`. +- **D-02:** Client, skill, and workspace operations are derived only from the + selected profile and manifest catalogs. No adapter may hard-code profile + membership. + +### Client Configuration Ownership +- **D-03:** Each supported client uses an explicit adapter that previews, + validates, backs up, atomically applies, verifies, and surgically removes + only CAS-owned configuration. +- **D-04:** CAS-owned client configuration is namespaced and carries stable + ownership and content-digest evidence. Unrelated keys and user changes are + preserved during apply, repair, and removal. +- **D-05:** Existing user-owned targets require a recoverable backup before + first modification. Backups are recovery evidence, not permission to replace + later unrelated user changes during uninstall. + +### Skills and Workspaces +- **D-06:** Skills and workspace conventions are installed only from + allowlisted manifest sources into approved CAS-managed boundaries. +- **D-07:** Skill and workspace resources receive the same deterministic plan, + ownership-ledger, drift-digest, safe-path, and uninstall-only-owned behavior + as other managed resources. +- **D-08:** Existing unowned skill or workspace targets fail closed on conflict; + CAS does not silently adopt or overwrite them. + +### MCP Security Boundary +- **D-09:** MCP configuration explicitly labels local workstation transports + (`stdio`) separately from production remote transports (`http` or `sse`). +- **D-10:** Generated configuration may contain non-secret authentication + references or instructions, but never credentials, access tokens, API keys, + or embedded secrets. + +### Planning, Drift, and Recovery +- **D-11:** Clients, skills, and workspaces become typed operations in + `New-CasOperationPlan` and execute through `Invoke-CasOperationPlan`; direct + mutation helpers must not bypass preview, journal, or correlation evidence. +- **D-12:** Inventory compares canonical managed content digests and reports + satisfied, missing, drifted, conflicting, or unsupported state. Repair + reconciles only CAS-owned drift. + +### the agent's Discretion +- Exact adapter function decomposition, namespaced JSON shape, and manifest + property names, provided they remain PowerShell 5.1-compatible, + deterministic, schema-validated, and fail closed. +- Whether the first implementation plan separates client adapters from + skill/workspace adapters, provided all CFG-01 through CFG-05 requirements are + covered by the completed phase. + +### Deferred Ideas (OUT OF SCOPE) +- Rich doctor findings and support bundles belong to Phase 5. +- Clean-machine end-to-end proof and trusted release artifacts belong to Phase 6. +- Central profile distribution and fleet management remain v2 (`PLAT-03`). + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| CFG-01 | CAS generates profile-specific configuration for supported AI clients without overwriting unrelated user configuration. | Use per-client adapters that own one namespaced MCP entry and perform read-modify-validate-stage-atomic-replace. | +| CFG-02 | User can preview, validate, apply, and remove only CAS-owned client configuration. | Add typed client operations, owned-node ledger evidence, staged verification, and surgical removal. | +| CFG-03 | Profiles install and validate portable agent skills and workspace conventions from allowlisted sources. | Add exact manifest source/target declarations and deterministic tree manifests with conflict detection. | +| CFG-04 | MCP configuration clearly distinguishes local workstation transports from production remote transports and never embeds secrets. | Normalize transport intent, render client-native shapes, and permit only environment-variable/auth references. | +| CFG-05 | Configuration adapters detect drift and preserve recoverable backups. | Digest only CAS-owned nodes/trees, classify inventory state, and retain first-modification recovery backups without whole-file uninstall restore. | + + +## Summary + +Phase 4 should extend the existing desired-state, inventory, planner, apply, and +ledger pipeline rather than expand `New-CasClientConfigs`. Today that helper +writes isolated fragments directly with `Set-Content`; the operation inventory, +planner, and executor currently handle only tools and repositories. The schemas +already permit generic `configuration`, `file`, and `directory` kinds, but the +planner and executor need explicit client, skill, and workspace adapter routing. +[VERIFIED: scripts/Cas.Workstation.psm1, schemas/operation-plan.schema.json] + +The safe ownership unit for a client file is one stable namespaced MCP server +entry, not the entire user file. Use `cas-workstation.prompt-refiner` as the +stable logical owner key, rendered into each client's native syntax. Inventory +and ledger evidence must digest that owned node only. Apply may atomically +replace the full physical file after a surgical semantic merge; uninstall must +remove only the owned node and must never restore the original whole-file +backup over later user changes. [VERIFIED: 04-CONTEXT.md D-03 through D-05] + +Skills and workspaces should use deterministic tree manifests: sorted normalized +relative paths plus per-file SHA-256 digests, with reparse points rejected. +Existing unowned targets are conflicts. Updates and removals act only on +ledger-owned entries, while unexpected files block destructive cleanup. +[VERIFIED: 04-CONTEXT.md D-06 through D-08; scripts/Cas.Workstation.psm1] + +**Primary recommendation:** Implement one shared owned-resource adapter contract, +three client-native adapters, and one tree-resource adapter, all invoked only as +typed operations through the current plan/apply/journal engine. [VERIFIED: +04-CONTEXT.md D-11 through D-12] + +## Project Constraints (from AGENTS.md) + +- Keep Windows 11 and Windows PowerShell 5.1 compatibility; PowerShell 7 is a + development shell, not the v1 host contract. [VERIFIED: AGENTS.md] +- Never embed credentials, access tokens, or machine-specific secrets. + [VERIFIED: AGENTS.md] +- Default destructive behavior to preview or explicit confirmation and preserve + unrelated user state. [VERIFIED: AGENTS.md] +- Keep generated state under configured CAS root/profile paths and derive + resources from declarative manifest data. [VERIFIED: AGENTS.md] +- Validate manifest and managed-state changes against JSON schemas, add Pester + tests for behavior and failure paths, and preserve non-interactive CI. + [VERIFIED: AGENTS.md] +- Use composition, guard clauses, testable core logic, and boundary-contained + side effects. Reproduce failure cases before fixes and validate with direct + tests/logs/runtime behavior. [VERIFIED: user-provided C:\PersonalRepo + AGENTS.md instructions] + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Resolve selected clients, skills, and workspaces | PowerShell domain/core | Manifest/schema | Resolution already derives all six categories from selected profiles. [VERIFIED: `Resolve-CasDesiredState`] | +| Inspect client-owned configuration | PowerShell adapter | Client-native config file | Adapter knows the native target and owned namespace; core consumes normalized status. [CITED: OpenAI/Anthropic/Gemini client docs] | +| Merge/remove client configuration | PowerShell adapter boundary | Atomic filesystem writer | Merge semantics are client-specific; replacement and backup remain shared safety primitives. [VERIFIED: 04-CONTEXT.md D-03] | +| Skill/workspace source validation and drift | PowerShell tree adapter | Managed repository/filesystem | Sources come from allowlisted repos; deterministic tree evidence drives status. [VERIFIED: stack.manifest.json, 04-CONTEXT.md D-06 through D-08] | +| Plan ordering and execution | Shared operation engine | Adapters | The planner owns deterministic sequencing; adapters execute one typed operation. [VERIFIED: `New-CasOperationPlan`, `Invoke-CasOperationPlan`] | +| Ownership, backup, and uninstall evidence | Managed-state ledger | Adapters | Ledger is the authority for owned node/path and digest evidence. [VERIFIED: schemas/managed-state.schema.json, 04-CONTEXT.md D-04 through D-05] | +| MCP transport/auth policy | Manifest semantic validator | Client renderers | Policy is client-independent; renderers translate approved intent to native syntax. [CITED: MCP transport and authorization specifications] | + +## Standard Stack + +### Core + +| Library/Capability | Version | Purpose | Why Standard | +|--------------------|---------|---------|--------------| +| Windows PowerShell | 5.1+ | Host automation and adapter orchestration | Locked v1 compatibility contract and currently available as 5.1.26100.8655. [VERIFIED: AGENTS.md; environment probe] | +| Pester | 5.7.1 | Unit, integration, and failure-path tests | Existing test framework; 46 current tests pass. [VERIFIED: module probe; `Invoke-Pester -Path tests`] | +| Newtonsoft.Json | 13.0.4, published 2025-09-16 | Fail-closed JSON token parsing and owned-node merge for Claude/Gemini | Windows PowerShell 5.1 `ConvertFrom-Json` keeps only the last duplicate key; Json.NET exposes duplicate-property handling and supports .NET Framework. Pin and vendor the approved DLL with checksum/provenance. [CITED: Microsoft ConvertFrom-Json docs; NuGet registry; Json.NET docs] | +| Existing CAS canonical JSON + SHA-256 helpers | repository-local | Stable desired/managed content digests | Existing tested implementation sorts object properties before compressed UTF-8 SHA-256 hashing. [VERIFIED: `ConvertTo-CasCanonicalJson`, `Get-CasSha256`, Manifest.Tests.ps1] | +| Existing `Write-CasAtomicJson` / `System.IO.File.Replace` pattern | repository-local / .NET Framework | Validate, stage, backup, and atomically replace JSON targets | Existing code and .NET API already create replacement backups. [VERIFIED: scripts/Cas.Workstation.psm1; CITED: Microsoft File.Replace docs] | +| Client-native Codex CLI staging | selected manifest tool | Safely modify TOML without hand-rolling TOML parsing | Codex officially supports managing MCP with `codex mcp` and stores configuration in `config.toml`. Run it against a staged `CODEX_HOME`, verify, then atomically replace the real target. [CITED: OpenAI Codex MCP docs] | + +### Supporting + +| Capability | Purpose | When to Use | +|------------|---------|-------------| +| JSON Schema Draft 2020-12 contracts | Extend manifest, managed-state, and operation-plan evidence | Every new property and typed operation must update positive/negative fixtures. [VERIFIED: repository schemas/tests] | +| `Get-FileHash -Algorithm SHA256` or equivalent stream hashing | Digest skill/workspace file bytes | Use for every tree-manifest file; do not hash timestamps or absolute source paths. [VERIFIED: Windows PowerShell 5.1 availability probe] | +| Client CLIs (`codex`, `claude`, `gemini`) | Post-stage/native validation where deterministic and non-secret | Apply verification only; planning must not run external processes. [VERIFIED: Phase 3 context; environment probe] | + +**Installation:** No runtime package-manager install should occur during Phase 4 +planning or apply. Vendor and pin the Json.NET DLL as a repository dependency so +safe JSON inspection is available before tool installation and without network +access. [VERIFIED: Phase 3 no-external-process planning decision; CITED: NuGet +Newtonsoft.Json 13.0.4] + +## Architecture Patterns + +### System Architecture Diagram + +```text +Selected profile + validated manifest + | + v + Resolve-CasDesiredState + | + v + Adapter registry resolves client / skill / workspace definition + | + v + Inventory: desired owned digest vs observed owned digest + ledger evidence + | + +--> satisfied ------> deterministic skip + +--> missing --------> create + +--> drifted --------> update only when CAS-owned + +--> conflicting ----> fail closed + +--> unsupported ----> fail closed for required resource + | + v + New-CasOperationPlan (dependency-aware deterministic typed operations) + | + preview ----+---- explicit apply + | + v + Invoke-CasOperationPlan + journal/events + | + v + stage -> validate -> backup -> atomic apply -> verify + | + v + update managed-state ownership/digest evidence +``` + +### Recommended Project Structure + +Keep the public module as the current integration surface, but decompose +functions by responsibility inside it unless the implementation plan explicitly +introduces dot-sourced private modules. [VERIFIED: repository convention] + +```text +scripts/Cas.Workstation.psm1 + manifest + semantic validation + adapter registry and normalized adapter results + client JSON adapter + Codex TOML-through-staged-CLI adapter + skill/workspace tree adapter + inventory / planner / executor integration + managed-state and uninstall integration + +tests/ + ClientConfig.Tests.ps1 + ManagedTrees.Tests.ps1 + Plan.Tests.ps1 + Apply.Tests.ps1 + Safety.Tests.ps1 + Uninstall.Tests.ps1 + Manifest.Tests.ps1 +``` + +### Pattern 1: Shared Owned-Resource Adapter Contract + +Use a normalized adapter result so core planning never knows client-native +syntax. [VERIFIED: 04-CONTEXT.md D-03, D-11] + +```powershell +# Recommended normalized result shape. +[pscustomobject]@{ + id = "client:codex" + kind = "configuration" + adapter = "codex-mcp" + target = $canonicalTarget + ownershipKey = "cas-workstation.prompt-refiner" + desiredDigest = $desiredOwnedNodeDigest + observedDigest = $observedOwnedNodeDigest + status = "satisfied" # missing|drifted|conflicting|unsupported + detail = $null +} +``` + +Required adapter operations: resolve exact target, render desired owned content, +inspect, stage apply, validate staged result, atomically commit, verify, and +surgically remove. [VERIFIED: 04-CONTEXT.md D-03 through D-05] + +### Pattern 2: Owned-Node JSON Merge + +For Claude and Gemini, parse the full target with duplicate-property rejection, +clone it, replace only `mcpServers["cas-workstation.prompt-refiner"]`, validate +the staged full document, then atomically replace the target. Preserve every +other semantic property. [CITED: Claude and Gemini MCP configuration docs; +Microsoft ConvertFrom-Json duplicate-key behavior; Json.NET duplicate-property +handling] + +The digest is over the canonical desired owned node, not over the full user +file. This makes unrelated user changes invisible to CAS drift while changes +inside the CAS namespace become drift. [VERIFIED: 04-CONTEXT.md D-04, D-12] + +### Pattern 3: Codex Native TOML Staging + +Codex stores MCP configuration in `~/.codex/config.toml`, with each server under +`[mcp_servers.]`. Do not implement a partial TOML parser. Copy the +user target to a CAS-controlled staging `CODEX_HOME`, invoke the allowlisted +`codex mcp` command against staging, validate the staged result, then atomically +replace the real file. [CITED: OpenAI Codex MCP docs] + +Planning uses manifest and file/ledger evidence only; the external Codex CLI may +run only inside the journaled apply adapter. [VERIFIED: Phase 3 context] + +### Pattern 4: Deterministic Tree Manifest for Skills and Workspaces + +Represent a desired tree as canonical records sorted by normalized relative +path: + +```powershell +[ordered]@{ + schemaVersion = "1.0.0" + resourceId = "skill:prompt-refiner" + entries = @( + [ordered]@{ path = "SKILL.md"; type = "file"; digest = "sha256:..." } + ) +} +``` + +Reject absolute paths, `..` escapes, reparse points, and any source outside the +manifest-declared managed repository. Digest the canonical record set. Install +files through staged copies and record owned files/directories so uninstall can +remove files first and only empty directories afterward. [VERIFIED: +`Assert-CasSafePath`, current uninstall behavior, 04-CONTEXT.md D-06 through +D-08] + +### Pattern 5: Dependency-Aware Deterministic Planning + +Typed resources require execution ordering: tools before client-native CLI +operations; repositories before repo-sourced skills/workspaces; skills and +workspaces before client configuration that references them. Current plans sort +only by operation ID, which can place client operations before repositories. +Add deterministic dependency evidence or a stable kind-priority/topological +sort and include that ordering in plan identity. [VERIFIED: +`New-CasOperationPlan`; stack.manifest.json] + +Recommended order: `tool -> repository -> skill/workspace -> client +configuration`. Cycles or missing dependencies fail before apply. [VERIFIED: +manifest relationships and locked fail-closed behavior] + +### Pattern 6: Backup Is Recovery Evidence, Not Uninstall State + +On first modification of a user-owned client file, retain a recoverable full +backup in a CAS-controlled backup directory and record it in the ledger. On +later updates, retain new operation backups as recovery evidence. Uninstall +parses the current file and removes only the CAS-owned node; it does not restore +the first backup. [VERIFIED: 04-CONTEXT.md D-05] + +### Anti-Patterns to Avoid + +- **Expanding `New-CasClientConfigs` direct writes:** it bypasses inventory, + preview, plan integrity, journal, ledger, and safe-path enforcement. + [VERIFIED: scripts/Cas.Workstation.psm1] +- **Digesting the entire client file:** unrelated user edits would be reported + as CAS drift. [VERIFIED: 04-CONTEXT.md D-04, D-12] +- **Restoring a whole client backup during uninstall:** this overwrites user + changes made after CAS first modified the file. [VERIFIED: 04-CONTEXT.md D-05] +- **Using Windows PowerShell `ConvertFrom-Json` alone for untrusted user + configuration:** duplicate keys are silently collapsed to the last key. + [CITED: Microsoft ConvertFrom-Json docs] +- **Treating all MCP transports as one shape:** client-native keys differ, and + Streamable HTTP replaced the old HTTP+SSE transport in the MCP specification. + [CITED: OpenAI/Claude/Gemini docs; MCP transport specification] +- **Recursive copy/remove of skill or workspace roots:** it can follow or erase + unexpected state and cannot prove per-entry ownership. [VERIFIED: + `Assert-CasSafePath`, `Invoke-CasUninstall`, 04-CONTEXT.md D-08] + +## Manifest and Contract Changes + +### Manifest + +Extend each client with an adapter ID, scope, exact target template, ownership +key, and supported transport mapping. Extend skills/workspaces with exact source +repository, source-relative path, target template, and adapter ID. Extend policy +with allowlisted adapter IDs, approved target templates/parents, and permitted +non-secret auth-reference field names. [VERIFIED: current manifest lacks these +properties; 04-CONTEXT.md D-02, D-06, D-09, D-10] + +The current `skills` entry identifies only `repo`, and the current `workspaces` +entry identifies only `relativePath`; neither identifies an installable source +tree. The current repository also contains no `cas-default` workspace source. +[VERIFIED: stack.manifest.json; repository grep] + +### Managed State + +Extend resource evidence to identify the adapter and owned unit, for example: +`adapter`, `ownershipKey` or `ownedPath`, `contentDigest`, and optional +`backupTarget`. Tree resources also need owned entry evidence or individual +file/directory ledger entries. [VERIFIED: current managed-state schema records +only whole target, ownership, backup, and digest] + +The current uninstall implementation restores the entire backup for every +modified resource; configuration adapters need a distinct surgical-remove path +so D-05 is not violated. [VERIFIED: `Get-CasUninstallPreview`, +`Invoke-CasUninstall`, 04-CONTEXT.md D-05] + +### Operation Plan + +Keep existing schema kinds (`configuration`, `file`, `directory`) and add +adapter/dependency metadata needed to execute typed Phase 4 operations without +embedding secret payloads in plans or logs. The operation source should identify +the manifest resource/source reference, not contain configuration content or +credentials. [VERIFIED: schemas/operation-plan.schema.json; 04-CONTEXT.md D-10, +D-11] + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| General TOML parsing/serialization | Regex or line-based TOML merger | Staged `codex mcp` native CLI adapter | Codex owns its TOML grammar and CLI management surface. [CITED: OpenAI Codex MCP docs] | +| JSON tokenization | Regex JSON merge or bare `ConvertFrom-Json` | Pinned Json.NET token model with duplicate-property errors | Duplicate keys and nested object semantics must fail closed. [CITED: Microsoft and Json.NET docs] | +| Whole-file JSON patch semantics | Generic RFC 7396 patch over user files | Exact owned-node replace/remove | Generic merge patch does not express CAS ownership or protect adjacent user state. [CITED: RFC 7396; VERIFIED: 04-CONTEXT.md D-04] | +| Tree identity | Timestamps, directory size, or unordered enumeration | Sorted relative-path + file-digest tree manifest | Stable content identity must be independent of machine and enumeration order. [VERIFIED: existing canonical digest pattern] | +| Recursive deletion | `Remove-Item -Recurse` on managed roots | Ledger-owned file removal then empty-directory removal | Unexpected user content must block deletion. [VERIFIED: Uninstall.Tests.ps1] | +| MCP authentication | Embedded tokens, API keys, or static bearer values | Environment-variable/auth references and client-native OAuth flow | MCP HTTP authorization is transport-level; stdio retrieves credentials from environment. [CITED: MCP authorization specification; 04-CONTEXT.md D-10] | + +## Common Pitfalls + +### Pitfall 1: Whole-File Ownership by Accident +**What goes wrong:** CAS reports drift for unrelated edits or overwrites user +settings during repair/uninstall. [VERIFIED: 04-CONTEXT.md D-04 through D-05] +**How to avoid:** Persist and compare the owned-node digest; merge/remove only +that node. +**Warning signs:** Ledger has only a target file path and whole-file digest. + +### Pitfall 2: Backup Restoration Violates Surgical Uninstall +**What goes wrong:** Restoring the first backup discards all later user edits. +[VERIFIED: current uninstall implementation versus 04-CONTEXT.md D-05] +**How to avoid:** Route configuration uninstall to the adapter's remove-owned +operation; retain backups only for explicit recovery. + +### Pitfall 3: Duplicate JSON Keys Are Silently Lost +**What goes wrong:** Windows PowerShell 5.1 keeps only the last duplicate key, +so a read-modify-write can destroy ambiguous user data. [CITED: Microsoft +ConvertFrom-Json docs] +**How to avoid:** Parse with duplicate-property handling set to error before +planning a merge or removal. + +### Pitfall 4: Planning Executes Client CLIs +**What goes wrong:** Preview mutates state, prompts, reads credentials, or +depends on tool availability. [VERIFIED: Phase 3 context] +**How to avoid:** Planning uses manifest, filesystem, and ledger evidence only; +native CLI execution is apply-time and journaled. + +### Pitfall 5: Resource Dependencies Are Hidden by ID Sorting +**What goes wrong:** A client operation runs before the repository or skill it +references exists. [VERIFIED: current operation ID sorting and manifest MCP +command path] +**How to avoid:** Add explicit deterministic dependencies or stable topological +ordering and test it. + +### Pitfall 6: Workspace/Skill Source Is Ambiguous +**What goes wrong:** Adapter hard-codes source locations or copies the wrong +variant. [VERIFIED: current manifest identifies no exact source paths; two +`prompt-refiner` skill variants exist in the sibling Promptimprover checkout] +**How to avoid:** Require exact source repo and relative source path in the +manifest; missing/ambiguous source fails semantic validation. + +### Pitfall 7: MCP Transport Labels Do Not Match Native Client Shape +**What goes wrong:** A generic `transport` property is written where the client +expects `command`, `url`, `httpUrl`, or `type`. [VERIFIED: +`New-CasClientConfigs`; CITED: client docs] +**How to avoid:** Store normalized intent in the manifest and render per-client: +Codex uses `command` or `url`; Claude uses command entries or HTTP type/url; +Gemini uses `command`, `url` for SSE, or `httpUrl` for Streamable HTTP. + +### Pitfall 8: Legacy SSE Is Presented as the Production Default +**What goes wrong:** New remote configurations are built around the transport +replaced by Streamable HTTP. [CITED: MCP transport specification] +**How to avoid:** Label `stdio` as workstation-local, use `http`/Streamable HTTP +as the production remote default, and retain `sse` only as an explicitly +legacy-compatible option. + +## Code Examples + +### Owned-Node Digest and Status + +```powershell +# Uses existing CAS canonicalization and SHA-256 helpers. +$desiredDigest = Get-CasSha256 -Value ( + ConvertTo-CasCanonicalJson -InputObject $desiredOwnedNode +) + +$status = if (-not $targetExists) { + "missing" +} +elseif (-not $ownedNodeExists -and $ledgerOwnsNode) { + "missing" +} +elseif (-not $ownedNodeExists) { + "missing" +} +elseif (-not $ledgerOwnsNode) { + "conflicting" +} +elseif ($observedOwnedDigest -ne $desiredDigest) { + "drifted" +} +else { + "satisfied" +} +``` + +Source: existing canonical digest helpers plus D-12 status contract. +[VERIFIED: scripts/Cas.Workstation.psm1; 04-CONTEXT.md] + +### Surgical JSON Apply + +```powershell +# Pseudocode: Json.NET load settings must reject duplicate property names. +$document = Read-CasJsonDocumentFailClosed -Path $target +$document.mcpServers[$ownershipKey] = $desiredOwnedNode + +$staged = Write-CasStagedJson -Document $document -Target $target +Assert-CasClientConfig -Adapter $adapter -Path $staged +$backup = Commit-CasAtomicFile -StagedPath $staged -TargetPath $target +Assert-CasOwnedNodeDigest -Path $target -ExpectedDigest $desiredDigest +``` + +Source: Json.NET duplicate-property handling, existing atomic replacement +pattern, and locked adapter behavior. [CITED: Json.NET docs; VERIFIED: +scripts/Cas.Workstation.psm1, 04-CONTEXT.md] + +### Secret-Free MCP Rendering + +```powershell +# Allowed: reference a variable name. Never resolve or persist its value. +[ordered]@{ + url = "https://example.internal/mcp" + bearer_token_env_var = "CAS_MCP_ACCESS_TOKEN" +} +``` + +Codex documents `bearer_token_env_var`; Gemini documents environment-variable +expansion; MCP authorization says stdio implementations retrieve credentials +from the environment. [CITED: OpenAI Codex MCP docs; Gemini MCP docs; MCP +authorization specification] + +## State of the Art + +| Old/Current Seed Approach | Required Phase 4 Approach | Impact | +|---------------------------|---------------------------|--------| +| Generic JSON fragment with `transport` property for every client | Client-native rendering and adapter validation | Current fragment shape is not a valid universal client contract. [VERIFIED: seed code; CITED: client docs] | +| Direct `Set-Content` from `New-CasClientConfigs` | Typed journaled adapter operation with staging, backup, atomic commit, and verification | Brings client mutation under Phase 3 guarantees. [VERIFIED: seed code; 04-CONTEXT.md D-11] | +| Whole-file backup restore for modified configuration | Surgical owned-node removal; backup retained for recovery only | Preserves later unrelated user changes. [VERIFIED: 04-CONTEXT.md D-05] | +| HTTP+SSE as a standard remote transport | Streamable HTTP as current standard; SSE only for backward compatibility | MCP specification states Streamable HTTP replaced HTTP+SSE. [CITED: MCP transport specification dated 2025-11-25] | +| Skill identified only by repo; workspace only by destination path | Exact allowlisted source tree + approved target + canonical tree digest | Makes install, drift, repair, and uninstall implementable. [VERIFIED: current manifest; 04-CONTEXT.md D-06 through D-08] | + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | The user-level native client targets are the intended Phase 4 integration scope, rather than project-only configuration files. [ASSUMED] | Architecture Patterns | Target templates and backup boundaries would change; adapter semantics remain the same. | +| A2 | `cas-workstation.prompt-refiner` is acceptable as the stable logical ownership key. [ASSUMED] | Summary | A different namespace changes manifests, ledger fixtures, and adapter tests but not architecture. | + +## Open Questions + +1. **Which exact source tree defines `cas-default` workspace conventions?** + - What we know: The manifest selects `cas-default`, but no source repo/path + or matching workspace content exists in this repository. [VERIFIED: + stack.manifest.json; repository grep] + - Recommendation: Make creation/selection of a minimal versioned workspace + source tree a Wave 0 task before CFG-03 implementation. + +2. **Which prompt-refiner skill variant is canonical?** + - What we know: The sibling Promptimprover checkout contains matching + `universal-refiner\skills\prompt-refiner` and + `gemini-extension\skills\prompt-refiner` trees; the current manifest names + only the repository. [VERIFIED: filesystem probe] + - Recommendation: Declare the exact canonical source-relative path in the + manifest; do not let the adapter infer it. + +3. **Should client integration target user scope or project scope?** + - What we know: Codex supports user and trusted-project config; Claude has + local/project/user MCP scopes; Gemini supports user and project settings. + [CITED: official client docs] + - Recommendation: Use user scope for the workstation-wide golden path unless + discuss-phase changes the decision; keep target scope declarative per + client. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|-------------|-----------|---------|----------| +| Windows PowerShell | Product host and tests | Yes | 5.1.26100.8655 | None | +| Pester | Validation | Yes | 5.7.1 | None | +| PSScriptAnalyzer | Full quality gate | Yes | 1.24.0 | None | +| Python/jsonschema | Existing schema fixtures | Yes | Python 3.14.2 | None | +| Git | Repo-sourced skills/workspaces | Yes | 2.53.0.windows.1 | None | +| Node.js | Local stdio MCP command | Yes | 24.13.0 | None | +| Codex CLI | Codex native config adapter verification | Yes | 0.138.0 | Required resource; fail unsupported if absent | +| Claude Code | Claude adapter verification | Yes | 2.1.172 | Required resource; fail unsupported if absent | +| Gemini CLI | Gemini adapter verification | Yes | 0.45.1 | Required resource; fail unsupported if absent | +| PowerShell 7 | Optional development shell | No | — | Windows PowerShell 5.1 is the supported host | + +All availability and versions were verified locally on 2026-06-13. The full +repository quality gate passed with 46/46 Pester tests, schema fixtures, static +analysis, and governance checks. [VERIFIED: environment probes and +`Invoke-Quality.ps1`] + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Pester 5.7.1 [VERIFIED: environment probe] | +| Config file | None; tests use direct Pester discovery and `Invoke-Quality.ps1`. [VERIFIED: repository] | +| Quick run command | `Invoke-Pester -Path tests\ClientConfig.Tests.ps1,tests\ManagedTrees.Tests.ps1,tests\Plan.Tests.ps1,tests\Uninstall.Tests.ps1` | +| Full suite command | `.\Invoke-Quality.ps1` | + +### Phase Requirements -> Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| CFG-01 | Profile-selected adapters merge native client configuration while preserving unrelated keys | integration/unit | `Invoke-Pester -Path tests\ClientConfig.Tests.ps1` | No - Wave 0 | +| CFG-02 | Preview/apply/remove operate only on CAS-owned namespace through plan/apply/ledger | integration | `Invoke-Pester -Path tests\ClientConfig.Tests.ps1,tests\Plan.Tests.ps1,tests\Apply.Tests.ps1,tests\Uninstall.Tests.ps1` | Partial - extend existing; add client tests | +| CFG-03 | Exact allowlisted skill/workspace sources install, validate, drift, conflict, and uninstall safely | integration/unit | `Invoke-Pester -Path tests\ManagedTrees.Tests.ps1,tests\Manifest.Tests.ps1,tests\Uninstall.Tests.ps1` | No - Wave 0 | +| CFG-04 | Local/remote transports render distinctly and generated output contains no secret values | unit/security | `Invoke-Pester -Path tests\ClientConfig.Tests.ps1,tests\Manifest.Tests.ps1` | No - Wave 0 | +| CFG-05 | Owned-node/tree drift is detected and first-modification backups remain recoverable without whole-file uninstall restore | integration | `Invoke-Pester -Path tests\ClientConfig.Tests.ps1,tests\ManagedTrees.Tests.ps1,tests\Safety.Tests.ps1,tests\Uninstall.Tests.ps1` | Partial - extend existing; add adapter tests | + +### Required Test Cases + +- Equivalent profile/inventory inputs produce byte-equivalent canonical plans + including Phase 4 operations. [VERIFIED: existing Plan.Tests.ps1 pattern] +- Preview performs no filesystem or external-process mutation. [VERIFIED: + existing OperationWorkflow.Tests.ps1 pattern] +- Unrelated JSON/TOML client settings survive apply, repair, and uninstall. + [VERIFIED: CFG-01/CFG-02 contract] +- Duplicate JSON properties, malformed config, unsupported adapter/transport, + and ambiguous ownership fail closed before mutation. [CITED: Microsoft + ConvertFrom-Json behavior; VERIFIED: phase decisions] +- First modification creates recoverable backup; later uninstall removes only + the owned node and preserves later unrelated changes. [VERIFIED: D-05] +- Changed owned node is `drifted`; changed unrelated node remains `satisfied`; + missing ledger with existing namespaced node is `conflicting`. [VERIFIED: + D-04, D-12] +- Skill/workspace tree digest ignores enumeration order but changes for file + content/path changes. [VERIFIED: deterministic digest contract] +- Existing unowned target, unexpected file, source escape, destination escape, + and reparse point all fail closed. [VERIFIED: D-06 through D-08 and current + safety tests] +- Operation order honors repository/tool dependencies and remains deterministic. + [VERIFIED: identified current ordering gap] +- Plan, journal, events, and generated config never contain seeded secret + values. [VERIFIED: D-10] + +### Sampling Rate + +- **Per task commit:** Run the focused new/extended Pester files for the adapter + or contract being changed. [VERIFIED: repository test pattern] +- **Per wave merge:** Run `Invoke-Pester -Path tests` plus schema fixture + validation. [VERIFIED: repository quality architecture] +- **Phase gate:** Run `.\Invoke-Quality.ps1`; full suite must remain green before + `$gsd-verify-work`. [VERIFIED: AGENTS.md and current passing quality gate] + +### Wave 0 Gaps + +- [ ] `tests\ClientConfig.Tests.ps1` - owned-node merges, native rendering, + backup, drift, secret rejection, and surgical removal. +- [ ] `tests\ManagedTrees.Tests.ps1` - source allowlist, canonical tree digest, + conflict, repair, and uninstall behavior. +- [ ] Extend manifest/managed-state/operation-plan positive and negative + fixtures before implementing adapters. +- [ ] Add a minimal canonical `cas-default` workspace source tree or fixture. +- [ ] Pin and load Json.NET 13.0.4 with checksum/provenance and a test proving + duplicate-property rejection under Windows PowerShell 5.1. + +## Security Domain + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|------------------| +| V2 Authentication | Yes for remote MCP references | Store only environment-variable/auth-flow references; never secret values. [CITED: MCP authorization spec; D-10] | +| V3 Session Management | Limited to remote MCP clients | Do not persist MCP session IDs or tokens in generated config/ledger/logs. [CITED: MCP transport specification] | +| V4 Access Control | Yes | Manifest allowlists, exact adapter/target/source boundaries, namespaced ownership, and client tool allowlists where supported. [VERIFIED: phase decisions; CITED: client docs] | +| V5 Input Validation | Yes | JSON Schema plus semantic validation; duplicate-property rejection; canonical path and reparse-point checks. [VERIFIED: repository patterns; CITED: Microsoft/Json.NET docs] | +| V6 Cryptography | Yes | Use SHA-256 for deterministic evidence; do not implement credentials or cryptography. [VERIFIED: existing helpers; D-10] | + +### Known Threat Patterns + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| User-config clobbering | Tampering | Owned-node merge/remove, staged validation, atomic replacement, recovery backup. [VERIFIED: D-03 through D-05] | +| Path escape or reparse-point traversal | Tampering/Elevation | Canonical exact-target/source checks and existing reparse-point rejection before every mutation. [VERIFIED: current safety implementation/tests] | +| Untrusted skill/workspace content replacement | Tampering | Allowlisted repository/path, canonical tree digest, conflict detection, ledger evidence. [VERIFIED: D-06 through D-08] | +| Secret leakage into config/plan/log | Information Disclosure | Permit names/references only; reject secret-like fields/values; test seeded canaries across outputs. [VERIFIED: D-10] | +| Remote MCP exposed without authentication | Spoofing/Information Disclosure | Production remote intent uses Streamable HTTP with proper authentication; local HTTP binds localhost and validates Origin. [CITED: MCP transport specification] | +| Overprivileged MCP tools | Elevation of Privilege | Preserve client approval defaults and use tool allowlists where declared; never set Gemini `trust=true` by default. [CITED: OpenAI and Gemini MCP docs] | +| Ambiguous duplicate JSON keys | Tampering | Reject duplicate properties before semantic merge. [CITED: Microsoft ConvertFrom-Json docs; Json.NET docs] | + +## Sources + +### Primary (HIGH confidence) + +- Repository `AGENTS.md`, `stack.manifest.json`, schemas, + `scripts/Cas.Workstation.psm1`, and Pester tests - current architecture, + contracts, gaps, and passing baseline. +- `.planning/phases/04-client-skills-and-workspace-profiles/04-CONTEXT.md` - + locked Phase 4 decisions. +- https://developers.openai.com/codex/mcp - Codex native MCP config, scopes, + stdio/HTTP fields, and environment auth references. +- https://code.claude.com/docs/en/mcp - Claude MCP scopes and native JSON shape. +- https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md + - Gemini native settings, transports, environment references, and trust/tool + controls. +- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports - + current standard transports and HTTP security requirements. +- https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization - + HTTP authorization scope and stdio environment-credential boundary. +- https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json?view=powershell-5.1 + - PowerShell 5.1 duplicate-key and comment behavior. +- https://learn.microsoft.com/en-us/dotnet/api/system.io.file.replace?view=netframework-4.8.1 + - replacement/backup behavior. +- https://www.nuget.org/packages/Newtonsoft.Json - verified stable version + 13.0.4, publish date, and .NET Framework compatibility. +- https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Linq_JsonLoadSettings_DuplicatePropertyNameHandling.htm + - duplicate-property handling control. + +### Secondary (MEDIUM confidence) + +- https://www.rfc-editor.org/rfc/rfc7396 - generic JSON Merge Patch semantics, + used only to explain why ownership-aware exact-node merge is required. +- https://www.rfc-editor.org/rfc/rfc8785 - canonical JSON reference; CAS should + continue its existing tested canonical subset unless cross-implementation JCS + interoperability becomes a requirement. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - verified against local environment, repository + implementation, Microsoft docs, client docs, and NuGet registry. +- Architecture: HIGH - primarily constrained by locked context and existing + plan/apply/ledger contracts. +- Pitfalls: HIGH - directly evidenced by current code gaps, PowerShell 5.1 + behavior, and official client/MCP documentation. + +**Research date:** 2026-06-13 +**Valid until:** 2026-07-13 for repository architecture; recheck client-native +configuration docs and MCP specification immediately before implementation +because those interfaces are fast-moving. diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-VALIDATION.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-VALIDATION.md new file mode 100644 index 0000000..3c4eca4 --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-VALIDATION.md @@ -0,0 +1,61 @@ +--- +phase: 4 +slug: client-skills-and-workspace-profiles +status: draft +nyquist_compliant: true +wave_0_complete: true +created: 2026-06-13 +--- + +# Phase 4 - Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Pester 5.7.1, PSScriptAnalyzer, JSON Schema Draft 2020-12 | +| **Config file** | `PSScriptAnalyzerSettings.psd1` | +| **Quick run command** | `Invoke-Pester -Path tests/ClientConfig.Tests.ps1,tests/ManagedTrees.Tests.ps1,tests/Plan.Tests.ps1,tests/Uninstall.Tests.ps1` | +| **Full suite command** | `powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-Quality.ps1` | +| **Estimated runtime** | ~20 seconds | + +## Sampling Rate + +- **After every task commit:** Run the focused Pester files changed by the task. +- **After every plan wave:** Run `Invoke-Quality.ps1`. +- **Before verification:** Full suite must be green. +- **Max feedback latency:** 30 seconds. + +## Per-Task Verification Map + +| Capability | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | Status | +|------------|-------------|------------|-----------------|-----------|-------------------|--------| +| Manifest adapter/source/target policy | CFG-03, CFG-04 | T-04-01 | Reject unallowlisted sources, targets, transports, and secret-bearing fields | schema + unit | `Invoke-Pester -Path tests/ContractSchemas.Tests.ps1,tests/Manifest.Tests.ps1` | pending | +| Surgical client merge and removal | CFG-01, CFG-02, CFG-05 | T-04-02 | Preserve unrelated settings and remove only CAS-owned subtree | unit + integration | `Invoke-Pester -Path tests/ClientConfig.Tests.ps1,tests/Uninstall.Tests.ps1` | pending | +| Managed skill/workspace trees | CFG-03, CFG-05 | T-04-03 | Reject reparse points, escapes, and unowned conflicts | unit + integration | `Invoke-Pester -Path tests/ManagedTrees.Tests.ps1,tests/Safety.Tests.ps1` | pending | +| Typed planning, apply, and repair | CFG-01 through CFG-05 | T-04-04 | No mutation outside validated journaled operations | integration | `Invoke-Pester -Path tests/Plan.Tests.ps1,tests/Apply.Tests.ps1,tests/OperationWorkflow.Tests.ps1` | pending | +| Full governance and regression gate | CFG-01 through CFG-05 | T-04-01 through T-04-04 | Contracts, static analysis, docs, and all behavior remain green | full gate | `powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-Quality.ps1` | pending | + +## Wave 0 Requirements + +Existing infrastructure covers all phase requirements. New focused test files +must be added with their corresponding implementation tasks. + +## Manual-Only Verifications + +All phase behaviors must have automated verification. Real client CLIs may be +used as additional release evidence later, but Phase 4 tests must not require +authenticated external services or mutate real user profiles. + +## Validation Sign-Off + +- [x] All capabilities have automated verification. +- [x] Sampling continuity prevents three consecutive tasks without automated verification. +- [x] Existing infrastructure covers Wave 0. +- [x] No watch-mode flags. +- [x] Feedback latency target is below 30 seconds. +- [x] `nyquist_compliant: true` set in frontmatter. + +**Approval:** approved 2026-06-13 diff --git a/.planning/phases/04-client-skills-and-workspace-profiles/04-VERIFICATION.md b/.planning/phases/04-client-skills-and-workspace-profiles/04-VERIFICATION.md new file mode 100644 index 0000000..4a3ffee --- /dev/null +++ b/.planning/phases/04-client-skills-and-workspace-profiles/04-VERIFICATION.md @@ -0,0 +1,28 @@ +# Phase 4 Verification + +**Status:** Passed +**Verified:** 2026-06-14 + +## Goal + +Profiles safely install and maintain AI-native client configuration, portable +skills, and workspace conventions without clobbering unrelated user state. + +## Requirement Evidence + +| Requirement | Evidence | +|-------------|----------| +| CFG-01 | `tests/ClientConfig.Tests.ps1`, `tests/Apply.Tests.ps1` | +| CFG-02 | `tests/ClientConfig.Tests.ps1`, `tests/Uninstall.Tests.ps1` | +| CFG-03 | `tests/ManagedTrees.Tests.ps1`, `tests/Manifest.Tests.ps1` | +| CFG-04 | `tests/Manifest.Tests.ps1`, manifest MCP scope/auth contracts | +| CFG-05 | `tests/ClientConfig.Tests.ps1`, `tests/ManagedTrees.Tests.ps1` | + +## Validation + +`powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-Quality.ps1` +passed with 59/59 Pester tests plus schemas, PSScriptAnalyzer, governance, and +documentation validation. + +No Azure resources were deployed and no real user-profile client files were +mutated during verification. diff --git a/README.md b/README.md index c37a91f..b46ab8c 100644 --- a/README.md +++ b/README.md @@ -121,3 +121,22 @@ prove a fast-forward relationship. The `full` profile is the declarative golden path and includes `cas-platform`, `cas-contracts`, `cas-evals`, and `cas-reference-product`. + +## Client, Skill, And Workspace Profiles + +The selected profile also resolves clients, portable skills, and workspace +conventions into typed preview-first operations. Client adapters manage only +the namespaced `cas-workstation.prompt-refiner` MCP entry, preserve unrelated +settings, atomically back up modified files, and record an owned-content digest +for drift repair. + +Skills and workspaces are copied only from manifest-allowlisted repositories +into approved CAS-managed boundaries. Existing unowned targets, unsafe relative +paths, reparse points, malformed client files, and conflicting owned namespaces +fail closed. + +The manifest distinguishes local workstation MCP (`stdio`) from production +remote transports and permits only non-secret authentication references. +Credentials, tokens, and API keys are never generated or embedded. Uninstall +removes the CAS-owned MCP namespace surgically instead of restoring a stale +whole-file backup over later user changes. diff --git a/docs/traceability.json b/docs/traceability.json index 8e4008a..672ebb1 100644 --- a/docs/traceability.json +++ b/docs/traceability.json @@ -300,72 +300,52 @@ { "id": "CFG-01", "phase": 4, - "status": "pending", + "status": "verified", "adrs": { }, - "tests": { - - }, - "evidence": { - - } + "tests": ["tests/ClientConfig.Tests.ps1", "tests/Plan.Tests.ps1", "tests/Apply.Tests.ps1"], + "evidence": [".\\Invoke-Quality.ps1"] }, { "id": "CFG-02", "phase": 4, - "status": "pending", + "status": "verified", "adrs": { }, - "tests": { - - }, - "evidence": { - - } + "tests": ["tests/ClientConfig.Tests.ps1", "tests/Uninstall.Tests.ps1"], + "evidence": [".\\Invoke-Quality.ps1"] }, { "id": "CFG-03", "phase": 4, - "status": "pending", + "status": "verified", "adrs": { }, - "tests": { - - }, - "evidence": { - - } + "tests": ["tests/ManagedTrees.Tests.ps1", "tests/Manifest.Tests.ps1", "tests/Plan.Tests.ps1"], + "evidence": [".\\Invoke-Quality.ps1"] }, { "id": "CFG-04", "phase": 4, - "status": "pending", + "status": "verified", "adrs": { }, - "tests": { - - }, - "evidence": { - - } + "tests": ["tests/Manifest.Tests.ps1", "tests/ClientConfig.Tests.ps1"], + "evidence": [".\\Invoke-Quality.ps1"] }, { "id": "CFG-05", "phase": 4, - "status": "pending", + "status": "verified", "adrs": { }, - "tests": { - - }, - "evidence": { - - } + "tests": ["tests/ClientConfig.Tests.ps1", "tests/ManagedTrees.Tests.ps1", "tests/Apply.Tests.ps1"], + "evidence": [".\\Invoke-Quality.ps1"] }, { "id": "DIAG-01", diff --git a/schemas/manifest.schema.json b/schemas/manifest.schema.json index 63ff01a..9a8dd07 100644 --- a/schemas/manifest.schema.json +++ b/schemas/manifest.schema.json @@ -17,19 +17,20 @@ "repos": { "type": "array", "items": { "$ref": "#/$defs/repository" } }, "services": { "type": "array", "items": { "$ref": "#/$defs/identified" } }, "clients": { "type": "array", "items": { "$ref": "#/$defs/client" } }, - "skills": { "type": "array", "items": { "$ref": "#/$defs/identified" } }, + "skills": { "type": "array", "items": { "$ref": "#/$defs/managedTree" } }, "workspaces": { "type": "array", "items": { "$ref": "#/$defs/workspace" } }, - "sharedMcpServer": { "type": "object", "required": ["name", "transport", "command", "args"], "properties": { "name": { "type": "string", "minLength": 1 }, "transport": { "enum": ["stdio", "http", "sse"] }, "command": { "type": "string", "minLength": 1 }, "args": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": false } + "sharedMcpServer": { "type": "object", "required": ["name", "transport", "scope", "authReference", "command", "args"], "properties": { "name": { "type": "string", "minLength": 1 }, "transport": { "enum": ["stdio", "http", "sse"] }, "scope": { "enum": ["local-workstation", "production-remote"] }, "authReference": { "type": ["string", "null"], "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }, "command": { "type": "string", "minLength": 1 }, "args": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": false } }, "$defs": { "identified": { "type": "object", "required": ["id"], "properties": { "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" } }, "additionalProperties": true }, "selection": { "type": "object", "required": ["required", "optional"], "properties": { "required": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, "optional": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } } }, "additionalProperties": false }, "profile": { "type": "object", "required": ["description", "tools", "repos", "services", "clients", "skills", "workspaces"], "properties": { "description": { "type": "string", "minLength": 1 }, "tools": { "$ref": "#/$defs/selection" }, "repos": { "$ref": "#/$defs/selection" }, "services": { "$ref": "#/$defs/selection" }, "clients": { "$ref": "#/$defs/selection" }, "skills": { "$ref": "#/$defs/selection" }, "workspaces": { "$ref": "#/$defs/selection" } }, "additionalProperties": false }, - "policy": { "type": "object", "required": ["allowedInstallerKinds", "allowedCommands", "allowedRepositoryPrefixes", "allowedConfigTargets"], "properties": { "allowedInstallerKinds": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["winget", "scoop", "npm", "manual"] } }, "allowedCommands": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, "allowedRepositoryPrefixes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^https://.+" } }, "allowedConfigTargets": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[^\\\\/:*?\"<>|]+$" } } }, "additionalProperties": false }, + "policy": { "type": "object", "required": ["allowedInstallerKinds", "allowedCommands", "allowedRepositoryPrefixes", "allowedConfigTargets", "allowedAdapters"], "properties": { "allowedInstallerKinds": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["winget", "scoop", "npm", "manual"] } }, "allowedCommands": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } }, "allowedRepositoryPrefixes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^https://.+" } }, "allowedConfigTargets": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^[^\\\\/:*?\"<>|]+$" } }, "allowedAdapters": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["json-mcp", "tree-copy"] } } }, "additionalProperties": false }, "installer": { "type": "object", "required": ["kind"], "properties": { "kind": { "enum": ["winget", "scoop", "npm", "manual"] }, "id": { "type": "string", "minLength": 1 }, "hint": { "type": "string", "minLength": 1 } }, "additionalProperties": false }, "tool": { "type": "object", "required": ["id", "displayName", "command", "versionArgs", "versionPattern", "minimumVersion", "installers"], "properties": { "id": { "type": "string", "minLength": 1 }, "displayName": { "type": "string", "minLength": 1 }, "command": { "type": "string", "minLength": 1 }, "versionArgs": { "type": "array", "items": { "type": "string" } }, "versionPattern": { "type": "string", "minLength": 1 }, "minimumVersion": { "type": "string", "minLength": 1 }, "installers": { "type": "array", "items": { "$ref": "#/$defs/installer" } } }, "additionalProperties": false }, "repository": { "type": "object", "required": ["id", "url", "defaultBranch"], "properties": { "id": { "type": "string", "minLength": 1 }, "url": { "type": "string", "pattern": "^https://.+\\.git$" }, "defaultBranch": { "type": "string", "minLength": 1 } }, "additionalProperties": false }, - "client": { "type": "object", "required": ["id", "fileName"], "properties": { "id": { "type": "string", "minLength": 1 }, "fileName": { "type": "string", "pattern": "^[^\\\\/:*?\"<>|]+$" } }, "additionalProperties": false }, - "workspace": { "type": "object", "required": ["id", "relativePath"], "properties": { "id": { "type": "string", "minLength": 1 }, "relativePath": { "type": "string", "minLength": 1 } }, "additionalProperties": false } + "client": { "type": "object", "required": ["id", "fileName", "adapter", "ownershipKey"], "properties": { "id": { "type": "string", "minLength": 1 }, "fileName": { "type": "string", "pattern": "^[^\\\\/:*?\"<>|]+$" }, "adapter": { "const": "json-mcp" }, "ownershipKey": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]+$" } }, "additionalProperties": false }, + "managedTree": { "type": "object", "required": ["id", "repo", "sourceRelativePath", "targetRelativePath", "adapter"], "properties": { "id": { "type": "string", "minLength": 1 }, "repo": { "type": "string", "minLength": 1 }, "sourceRelativePath": { "type": "string", "pattern": "^(?![A-Za-z]:|[\\\\/]|.*\\.\\.).+$" }, "targetRelativePath": { "type": "string", "pattern": "^(?![A-Za-z]:|[\\\\/]|.*\\.\\.).+$" }, "adapter": { "const": "tree-copy" } }, "additionalProperties": false }, + "workspace": { "$ref": "#/$defs/managedTree" } } } diff --git a/schemas/operation-plan.schema.json b/schemas/operation-plan.schema.json index 77bbf63..135e4dc 100644 --- a/schemas/operation-plan.schema.json +++ b/schemas/operation-plan.schema.json @@ -14,6 +14,6 @@ "rootPath": { "type": "string", "minLength": 1 }, "configPath": { "type": "string", "minLength": 1 }, "desiredStateDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, - "operations": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "kind", "target", "risk", "action", "command", "source", "reason"], "properties": { "id": { "type": "string" }, "kind": { "enum": ["tool", "repository", "directory", "file", "configuration"] }, "target": { "type": "string" }, "risk": { "enum": ["low", "medium", "high"] }, "action": { "enum": ["create", "update", "remove", "skip"] }, "command": { "type": "string" }, "source": { "type": "string" }, "reason": { "type": "string" }, "defaultBranch": { "type": "string" } } } } + "operations": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["id", "kind", "target", "risk", "action", "command", "source", "reason"], "properties": { "id": { "type": "string" }, "kind": { "enum": ["tool", "repository", "directory", "file", "configuration"] }, "target": { "type": "string" }, "risk": { "enum": ["low", "medium", "high"] }, "action": { "enum": ["create", "update", "remove", "skip"] }, "command": { "type": "string" }, "source": { "type": "string" }, "reason": { "type": "string" }, "defaultBranch": { "type": "string" }, "adapter": { "enum": ["json-mcp", "tree-copy"] }, "resourceCategory": { "enum": ["client", "skill", "workspace"] }, "ownershipKey": { "type": "string" }, "desiredDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }, "observedDigest": { "type": ["string", "null"], "pattern": "^sha256:[a-f0-9]{64}$" } } } } } } diff --git a/scripts/Cas.Workstation.psm1 b/scripts/Cas.Workstation.psm1 index 37dcbd9..8adabba 100644 --- a/scripts/Cas.Workstation.psm1 +++ b/scripts/Cas.Workstation.psm1 @@ -84,11 +84,43 @@ function Assert-CasManifest { if (@($Manifest.policy.allowedConfigTargets) -notcontains $client.fileName) { throw "Client '$($client.id)' uses unallowlisted configuration target '$($client.fileName)'." } + if (@($Manifest.policy.allowedAdapters) -notcontains $client.adapter) { + throw "Client '$($client.id)' uses unallowlisted adapter '$($client.adapter)'." + } + if ($client.ownershipKey -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]+$') { + throw "Client '$($client.id)' uses an invalid ownership key." + } + } + + $knownRepos = @($Manifest.repos | ForEach-Object id) + foreach ($category in @("skills", "workspaces")) { + foreach ($resource in @($Manifest.$category)) { + if (@($Manifest.policy.allowedAdapters) -notcontains $resource.adapter) { + throw "$category resource '$($resource.id)' uses unallowlisted adapter '$($resource.adapter)'." + } + if ($knownRepos -notcontains $resource.repo) { + throw "$category resource '$($resource.id)' references unknown repository '$($resource.repo)'." + } + foreach ($relativePath in @($resource.sourceRelativePath, $resource.targetRelativePath)) { + if ([IO.Path]::IsPathRooted($relativePath) -or $relativePath -match '(^|[\\/])\.\.([\\/]|$)') { + throw "$category resource '$($resource.id)' uses unsafe relative path '$relativePath'." + } + } + } } if ($allowedCommands -notcontains $Manifest.sharedMcpServer.command) { throw "Shared MCP server uses unallowlisted command '$($Manifest.sharedMcpServer.command)'." } + if ($Manifest.sharedMcpServer.scope -eq "local-workstation" -and $Manifest.sharedMcpServer.transport -ne "stdio") { + throw "Local workstation MCP servers must use stdio transport." + } + if ($Manifest.sharedMcpServer.scope -eq "production-remote" -and $Manifest.sharedMcpServer.transport -eq "stdio") { + throw "Production remote MCP servers cannot use stdio transport." + } + if ($Manifest.sharedMcpServer.authReference -and $Manifest.sharedMcpServer.authReference -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { + throw "Shared MCP server authentication must be an environment reference, not a secret value." + } foreach ($profileName in Get-CasPropertyNames -InputObject $Manifest.profiles) { $profile = $Manifest.profiles.PSObject.Properties[$profileName].Value @@ -499,7 +531,15 @@ function Get-CasUninstallPreview { } $target = Assert-CasSafePath -Path $resource.target -ApprovedRoots $ApprovedRoots -AllowBoundary - if ($resource.ownership -eq "modified") { + if ($resource.kind -eq "directory" -and $resource.ownership -eq "created" -and $resource.contentDigest) { + $backup = $null + $action = "remove-owned-tree" + } + elseif ($resource.kind -eq "configuration" -and $resource.id -like "client:*") { + $backup = $null + $action = "remove-owned-configuration" + } + elseif ($resource.ownership -eq "modified") { $backup = Assert-CasSafePath -Path $resource.backupTarget -ApprovedRoots $ApprovedRoots if (-not (Test-Path -LiteralPath $backup -PathType Leaf)) { throw "Backup for modified resource '$($resource.id)' was not found: $backup" @@ -517,6 +557,7 @@ function Get-CasUninstallPreview { ownership = $resource.ownership target = $target backupTarget = $backup + contentDigest = $resource.contentDigest action = $action actionable = $true }) @@ -573,7 +614,24 @@ function Invoke-CasUninstall { continue } - if ($action.action -eq "restore-backup") { + if ($action.action -eq "remove-owned-tree") { + $actualDigest = Get-CasTreeDigest -Path $target -ApprovedRoots $ApprovedRoots + if (-not $action.contentDigest -or $actualDigest -ne $action.contentDigest) { + throw "Managed tree '$($action.id)' does not match ledger ownership evidence." + } + Remove-Item -LiteralPath $target -Recurse -Force + } + elseif ($action.action -eq "remove-owned-configuration") { + $clientId = $action.id.Substring("client:".Length) + $client = (Get-CasManifest).clients | Where-Object id -eq $clientId | Select-Object -First 1 + if (-not $client) { throw "Client adapter '$clientId' was not found for uninstall." } + if (Test-Path -LiteralPath $target -PathType Leaf) { + $existing = Get-Content -LiteralPath $target -Raw -Encoding UTF8 | ConvertFrom-Json + $updated = Remove-CasClientConfiguration -ExistingConfiguration $existing -OwnershipKey $client.ownershipKey + $null = Write-CasAtomicJson -InputObject $updated -Path $target -ApprovedRoots $ApprovedRoots + } + } + elseif ($action.action -eq "restore-backup") { $backup = Assert-CasSafePath -Path $action.backupTarget -ApprovedRoots $ApprovedRoots if (-not (Test-Path -LiteralPath $backup -PathType Leaf)) { throw "Backup for '$($action.id)' disappeared before apply." @@ -986,14 +1044,217 @@ function Write-CasDoctorReport { $Report } +function Get-CasClientTarget { + param( + [Parameter(Mandatory = $true)][pscustomobject]$Client, + [Parameter(Mandatory = $true)][string]$ConfigPath, + [pscustomobject]$Manifest = (Get-CasManifest) + ) + + Join-Path (Join-Path (Join-Path $ConfigPath $Manifest.paths.mcp) "clients") $Client.fileName +} + +function Get-CasDesiredMcpNode { + param([pscustomobject]$Manifest = (Get-CasManifest)) + + [ordered]@{ + command = $Manifest.sharedMcpServer.command + args = @($Manifest.sharedMcpServer.args) + transport = $Manifest.sharedMcpServer.transport + scope = $Manifest.sharedMcpServer.scope + authReference = $Manifest.sharedMcpServer.authReference + } +} + +function Get-CasObjectPropertyValue { + param([object]$InputObject, [string]$Name) + + if ($null -eq $InputObject) { return $null } + $property = $InputObject.PSObject.Properties[$Name] + if ($property) { $property.Value } +} + +function Merge-CasClientConfiguration { + param( + [object]$ExistingConfiguration, + [Parameter(Mandatory = $true)][pscustomobject]$Client, + [pscustomobject]$Manifest = (Get-CasManifest) + ) + + $merged = if ($null -eq $ExistingConfiguration) { + [pscustomobject]@{} + } + else { + $ExistingConfiguration | ConvertTo-Json -Depth 30 | ConvertFrom-Json + } + $servers = Get-CasObjectPropertyValue -InputObject $merged -Name "mcpServers" + if ($null -eq $servers) { + $servers = [pscustomobject]@{} + $merged | Add-Member -MemberType NoteProperty -Name "mcpServers" -Value $servers + } + elseif ($servers -isnot [pscustomobject]) { + throw "Client '$($Client.id)' has an invalid mcpServers object." + } + + $node = [pscustomobject](Get-CasDesiredMcpNode -Manifest $Manifest) + $property = $servers.PSObject.Properties[$Client.ownershipKey] + if ($property) { + $property.Value = $node + } + else { + $servers | Add-Member -MemberType NoteProperty -Name $Client.ownershipKey -Value $node + } + $merged +} + +function Remove-CasClientConfiguration { + param( + [Parameter(Mandatory = $true)][object]$ExistingConfiguration, + [Parameter(Mandatory = $true)][string]$OwnershipKey + ) + + $updated = $ExistingConfiguration | ConvertTo-Json -Depth 30 | ConvertFrom-Json + $servers = Get-CasObjectPropertyValue -InputObject $updated -Name "mcpServers" + if ($servers -and $servers.PSObject.Properties[$OwnershipKey]) { + $servers.PSObject.Properties.Remove($OwnershipKey) + } + $updated +} + +function Get-CasClientConfigurationStatus { + param( + [Parameter(Mandatory = $true)][pscustomobject]$Client, + [Parameter(Mandatory = $true)][string]$ConfigPath, + [pscustomobject]$Manifest = (Get-CasManifest) + ) + + $target = Get-CasClientTarget -Client $Client -ConfigPath $ConfigPath -Manifest $Manifest + $desiredDigest = Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject (Get-CasDesiredMcpNode -Manifest $Manifest)) + if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { + return [pscustomobject]@{ status = "missing"; target = $target; desiredDigest = $desiredDigest; observedDigest = $null } + } + try { + $configuration = Get-Content -LiteralPath $target -Raw -Encoding UTF8 | ConvertFrom-Json + } + catch { + return [pscustomobject]@{ status = "unsupported"; target = $target; desiredDigest = $desiredDigest; observedDigest = $null } + } + $servers = Get-CasObjectPropertyValue -InputObject $configuration -Name "mcpServers" + $node = Get-CasObjectPropertyValue -InputObject $servers -Name $Client.ownershipKey + if ($null -eq $node) { + return [pscustomobject]@{ status = "missing"; target = $target; desiredDigest = $desiredDigest; observedDigest = $null } + } + $observedDigest = Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject $node) + [pscustomobject]@{ + status = if ($observedDigest -eq $desiredDigest) { "satisfied" } else { "drifted" } + target = $target + desiredDigest = $desiredDigest + observedDigest = $observedDigest + } +} + +function Set-CasClientConfiguration { + param( + [Parameter(Mandatory = $true)][pscustomobject]$Client, + [Parameter(Mandatory = $true)][string]$ConfigPath, + [string[]]$ApprovedRoots = @($ConfigPath), + [pscustomobject]$Manifest = (Get-CasManifest) + ) + + $target = Get-CasClientTarget -Client $Client -ConfigPath $ConfigPath -Manifest $Manifest + $parent = Split-Path -Parent $target + $null = Assert-CasSafePath -Path $parent -ApprovedRoots $ApprovedRoots -AllowBoundary + if (-not (Test-Path -LiteralPath $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + $existing = if (Test-Path -LiteralPath $target -PathType Leaf) { + Get-Content -LiteralPath $target -Raw -Encoding UTF8 | ConvertFrom-Json + } + else { $null } + $merged = Merge-CasClientConfiguration -ExistingConfiguration $existing -Client $Client -Manifest $Manifest + $backup = Write-CasAtomicJson -InputObject $merged -Path $target -ApprovedRoots $ApprovedRoots + $status = Get-CasClientConfigurationStatus -Client $Client -ConfigPath $ConfigPath -Manifest $Manifest + if ($status.status -ne "satisfied") { + throw "Client configuration '$($Client.id)' failed post-write verification." + } + [pscustomobject]@{ target = $target; backupTarget = $backup; contentDigest = $status.desiredDigest; wasPresentBefore = [bool]$existing } +} + +function Get-CasTreeDigest { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string[]]$ApprovedRoots + ) + + $root = Assert-CasSafePath -Path $Path -ApprovedRoots $ApprovedRoots + if (-not (Test-Path -LiteralPath $root -PathType Container)) { + return $null + } + $entries = @( + Get-ChildItem -LiteralPath $root -Recurse -File | Sort-Object FullName | ForEach-Object { + $null = Assert-CasSafePath -Path $_.FullName -ApprovedRoots $root + [ordered]@{ + path = $_.FullName.Substring($root.Length).TrimStart("\", "/").Replace("\", "/") + digest = "sha256:$((Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant())" + } + } + ) + Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject $entries) +} + +function Copy-CasManagedTree { + param( + [Parameter(Mandatory = $true)][string]$Source, + [Parameter(Mandatory = $true)][string]$Target, + [Parameter(Mandatory = $true)][string[]]$ApprovedRoots, + [switch]$ReplaceOwned, + [string]$ExpectedOwnedDigest + ) + + $sourceRoot = Assert-CasSafePath -Path $Source -ApprovedRoots $ApprovedRoots + $targetRoot = Assert-CasSafePath -Path $Target -ApprovedRoots $ApprovedRoots + if (-not (Test-Path -LiteralPath $sourceRoot -PathType Container)) { + throw "Managed tree source was not found: $sourceRoot" + } + if ((Test-Path -LiteralPath $targetRoot) -and -not $ReplaceOwned) { + throw "Managed tree target already exists and cannot be adopted: $targetRoot" + } + if (-not (Test-Path -LiteralPath $targetRoot)) { + New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null + } + if ($ReplaceOwned) { + $actualDigest = Get-CasTreeDigest -Path $targetRoot -ApprovedRoots $ApprovedRoots + if (-not $ExpectedOwnedDigest -or $actualDigest -ne $ExpectedOwnedDigest) { + throw "Managed tree target does not match prior ownership evidence." + } + Remove-Item -LiteralPath $targetRoot -Recurse -Force + New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null + } + foreach ($file in Get-ChildItem -LiteralPath $sourceRoot -Recurse -File) { + $null = Assert-CasSafePath -Path $file.FullName -ApprovedRoots $sourceRoot + $relative = $file.FullName.Substring($sourceRoot.Length).TrimStart("\", "/") + $destination = Join-Path $targetRoot $relative + $null = Assert-CasSafePath -Path $destination -ApprovedRoots $targetRoot + $parent = Split-Path -Parent $destination + if (-not (Test-Path -LiteralPath $parent -PathType Container)) { + New-Item -ItemType Directory -Path $parent -Force | Out-Null + } + Copy-Item -LiteralPath $file.FullName -Destination $destination -Force + } + [pscustomobject]@{ target = $targetRoot; contentDigest = Get-CasTreeDigest -Path $targetRoot -ApprovedRoots $ApprovedRoots; wasPresentBefore = $false } +} + function Get-CasOperationInventory { param( [string]$Profile = "full", [string]$RootPath = (Get-CasDefaultRootPath), + [string]$ConfigPath = (Get-CasDefaultConfigPath), [pscustomobject]$Manifest = (Get-CasManifest) ) $resources = New-Object System.Collections.Generic.List[object] + $managedStatePath = Get-CasManagedStatePath -ConfigPath $ConfigPath -Manifest $Manifest + $managedState = if (Test-Path -LiteralPath $managedStatePath -PathType Leaf) { Read-CasManagedState -Path $managedStatePath } else { $null } foreach ($tool in Get-CasProfileToolDefinitions -Profile $Profile -Manifest $Manifest) { $status = Get-CasToolStatus -Tool $tool $null = $resources.Add([pscustomobject]@{ id = "tool:$($tool.id)"; status = $status.status; detail = $status.installedVersion }) @@ -1008,6 +1269,31 @@ function Get-CasOperationInventory { } $null = $resources.Add([pscustomobject]@{ id = "repo:$($repo.id)"; status = $status; detail = $path }) } + $profileDefinition = Get-CasProfile -Name $Profile -Manifest $Manifest + $selectedRepoIds = @($profileDefinition.repos.required) + @($profileDefinition.repos.optional) + foreach ($clientId in @($profileDefinition.clients.required) + @($profileDefinition.clients.optional)) { + $client = $Manifest.clients | Where-Object id -eq $clientId | Select-Object -First 1 + $status = Get-CasClientConfigurationStatus -Client $client -ConfigPath $ConfigPath -Manifest $Manifest + if ($status.status -eq "drifted" -and ($null -eq $managedState -or @($managedState.resources | Where-Object id -eq "client:$clientId").Count -eq 0)) { + $status.status = "conflicting" + } + $null = $resources.Add([pscustomobject]@{ id = "client:$clientId"; status = $status.status; detail = $status.target; desiredDigest = $status.desiredDigest }) + } + foreach ($category in @("skills", "workspaces")) { + foreach ($id in @($profileDefinition.$category.required) + @($profileDefinition.$category.optional)) { + $definition = $Manifest.$category | Where-Object id -eq $id | Select-Object -First 1 + $source = Join-Path (Join-Path (Join-Path $RootPath $Manifest.paths.reposRoot) $definition.repo) $definition.sourceRelativePath + $target = Join-Path $ConfigPath $definition.targetRelativePath + $sourceDigest = $null + $targetDigest = $null + $status = if (-not (Test-Path -LiteralPath $source -PathType Container)) { if ($selectedRepoIds -contains $definition.repo) { "pending-source" } else { "unsupported" } } elseif (-not (Test-Path -LiteralPath $target -PathType Container)) { "missing" } else { + $sourceDigest = Get-CasTreeDigest -Path $source -ApprovedRoots $RootPath + $targetDigest = Get-CasTreeDigest -Path $target -ApprovedRoots $ConfigPath + if ($sourceDigest -eq $targetDigest) { "satisfied" } elseif ($managedState -and @($managedState.resources | Where-Object id -eq "$($category.TrimEnd('s')):$id").Count -gt 0) { "drifted" } else { "conflicting" } + } + $null = $resources.Add([pscustomobject]@{ id = "$($category.TrimEnd('s')):$id"; status = $status; detail = $target; desiredDigest = $sourceDigest; observedDigest = $targetDigest }) + } + } [pscustomobject]@{ resources = $resources.ToArray() } } @@ -1063,10 +1349,49 @@ function New-CasOperationPlan { defaultBranch = $resource.definition.defaultBranch }) } + "clients" { + $status = if ($actual.Count -gt 0 -and $actual[0].status -eq "synchronized") { "satisfied" } elseif ($actual.Count -gt 0) { $actual[0].status } else { "missing" } + if ($status -in @("unsupported", "conflicting")) { throw "Client '$($resource.id)' has $status configuration state." } + $target = Get-CasClientTarget -Client $resource.definition -ConfigPath $ConfigPath -Manifest $Manifest + $null = $operations.Add([ordered]@{ + id = "client:$($resource.id)" + kind = "configuration" + resourceCategory = "client" + adapter = $resource.definition.adapter + ownershipKey = $resource.definition.ownershipKey + target = $target + risk = if ($status -eq "satisfied") { "low" } else { "medium" } + action = if ($status -eq "satisfied") { "skip" } elseif ($status -eq "missing") { "create" } else { "update" } + command = "merge CAS-owned MCP configuration" + source = "manifest:sharedMcpServer" + desiredDigest = if ($actual.Count -gt 0 -and $actual[0].PSObject.Properties["desiredDigest"]) { $actual[0].desiredDigest } else { Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject (Get-CasDesiredMcpNode -Manifest $Manifest)) } + reason = if ($status -eq "satisfied") { "CAS-owned client configuration is satisfied." } else { "CAS-owned client configuration is missing or drifted." } + }) + } + { $_ -in @("skills", "workspaces") } { + $status = if ($actual.Count -gt 0 -and $actual[0].status -eq "synchronized") { "satisfied" } elseif ($actual.Count -gt 0) { $actual[0].status } else { "missing" } + if ($status -in @("conflicting", "unsupported")) { throw "$($resource.category.TrimEnd('s')) '$($resource.id)' has $status state and cannot be reconciled safely." } + $source = Join-Path (Join-Path (Join-Path $RootPath $Manifest.paths.reposRoot) $resource.definition.repo) $resource.definition.sourceRelativePath + $target = Join-Path $ConfigPath $resource.definition.targetRelativePath + $null = $operations.Add([ordered]@{ + id = "$($resource.category.TrimEnd('s')):$($resource.id)" + kind = "directory" + resourceCategory = $resource.category.TrimEnd("s") + adapter = $resource.definition.adapter + target = $target + risk = if ($status -eq "satisfied") { "low" } else { "medium" } + action = if ($status -eq "satisfied") { "skip" } elseif ($status -eq "drifted") { "update" } else { "create" } + command = "copy allowlisted managed tree" + source = $source + observedDigest = if ($actual.Count -gt 0 -and $actual[0].PSObject.Properties["observedDigest"]) { $actual[0].observedDigest } else { $null } + reason = if ($status -eq "satisfied") { "Managed tree is satisfied." } else { "Managed tree is missing." } + }) + } } } - $sortedOperations = @($operations.ToArray() | Sort-Object { $_.id }) + $kindOrder = @{ tool = 0; repository = 1; directory = 2; configuration = 3 } + $sortedOperations = @($operations.ToArray() | Sort-Object @{ Expression = { $kindOrder[$_.kind] } }, @{ Expression = { $_.id } }) $identity = [ordered]@{ schemaVersion = "1.0.0" mode = $Mode @@ -1213,7 +1538,10 @@ function Read-CasOperationJournal { } function Invoke-CasPlannedOperation { - param([Parameter(Mandatory = $true)][pscustomobject]$Operation) + param( + [Parameter(Mandatory = $true)][pscustomobject]$Operation, + [pscustomobject]$Manifest = (Get-CasManifest) + ) if ($Operation.action -eq "skip") { return @@ -1244,9 +1572,47 @@ function Invoke-CasPlannedOperation { if ($LASTEXITCODE -ne 0) { throw "Repository operation '$($Operation.id)' failed with exit code $LASTEXITCODE." } return } + if ($Operation.kind -eq "configuration" -and $Operation.adapter -eq "json-mcp") { + $clientId = $Operation.id.Substring("client:".Length) + $client = $Manifest.clients | Where-Object id -eq $clientId | Select-Object -First 1 + $configPath = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Operation.target)) + return Set-CasClientConfiguration -Client $client -ConfigPath $configPath -ApprovedRoots $configPath -Manifest $Manifest + } + if ($Operation.kind -eq "directory" -and $Operation.adapter -eq "tree-copy") { + $observedDigest = if ($Operation.PSObject.Properties["observedDigest"]) { $Operation.observedDigest } else { $null } + return Copy-CasManagedTree -Source $Operation.source -Target $Operation.target -ApprovedRoots @((Split-Path -Parent $Operation.source), (Split-Path -Parent $Operation.target)) -ReplaceOwned:($Operation.action -eq "update") -ExpectedOwnedDigest $observedDigest + } throw "No executor is registered for operation kind '$($Operation.kind)'." } +function Update-CasManagedStateFromOperation { + param( + [Parameter(Mandatory = $true)][pscustomobject]$Plan, + [Parameter(Mandatory = $true)][pscustomobject]$Operation, + [AllowNull()][object]$Result, + [Parameter(Mandatory = $true)][string]$ConfigPath, + [Parameter(Mandatory = $true)][string[]]$ApprovedRoots, + [pscustomobject]$Manifest = (Get-CasManifest) + ) + + if ($null -eq $Result -or -not $Result.PSObject.Properties["target"]) { return } + $statePath = Get-CasManagedStatePath -ConfigPath $ConfigPath -Manifest $Manifest + $state = if (Test-Path -LiteralPath $statePath -PathType Leaf) { + Read-CasManagedState -Path $statePath + } + else { + New-CasManagedState -BundleId $Manifest.bundleId -Profile $Plan.profile -DesiredStateDigest $Plan.desiredStateDigest + } + $state.resources = @($state.resources | Where-Object id -ne $Operation.id) + $wasPresent = [bool]$Result.wasPresentBefore + $ownership = if ($wasPresent) { "modified" } else { "created" } + $kind = if ($Operation.kind -eq "configuration") { "configuration" } else { "directory" } + $backupTarget = if ($Result.PSObject.Properties["backupTarget"]) { $Result.backupTarget } else { $null } + $contentDigest = if ($Result.PSObject.Properties["contentDigest"]) { $Result.contentDigest } else { $null } + $null = Add-CasManagedResource -State $state -Id $Operation.id -Kind $kind -Ownership $ownership -Target $Result.target -WasPresentBefore $wasPresent -BackupTarget $backupTarget -ContentDigest $contentDigest + $null = Write-CasManagedState -State $state -Path $statePath -ApprovedRoots $ApprovedRoots +} + function Invoke-CasOperationPlan { param( [Parameter(Mandatory = $true)][pscustomobject]$Plan, @@ -1254,11 +1620,14 @@ function Invoke-CasOperationPlan { [string[]]$ApprovedRoots, [ValidateRange(0, 3)][int]$MaxRetries = 1, [switch]$Resume, - [scriptblock]$OperationHandler = { param($operation) Invoke-CasPlannedOperation -Operation $operation }, + [scriptblock]$OperationHandler, [pscustomobject]$Manifest = (Get-CasManifest) ) $null = Assert-CasOperationPlan -Plan $Plan + if (-not $OperationHandler) { + $OperationHandler = { param($operation) Invoke-CasPlannedOperation -Operation $operation -Manifest $Manifest }.GetNewClosure() + } if (-not $ApprovedRoots) { $ApprovedRoots = @($Plan.rootPath, $ConfigPath) } @@ -1307,7 +1676,8 @@ function Invoke-CasOperationPlan { Write-CasOperationJournal -Journal $journal -Path $paths.journal -ApprovedRoots $ApprovedRoots $null = Write-CasOperationEvent -Path $paths.events -CorrelationId $journal.correlationId -EventType $operation.id -Outcome "started" -Message "Operation attempt $($entry.attempts) started." -Metadata @{ command = $operation.command; source = $operation.source } try { - & $OperationHandler $operation + $operationResult = & $OperationHandler $operation + Update-CasManagedStateFromOperation -Plan $Plan -Operation $operation -Result $operationResult -ConfigPath $ConfigPath -ApprovedRoots $ApprovedRoots -Manifest $Manifest $entry.status = "succeeded" $entry.lastError = $null $entry.guidance = "No recovery action required." @@ -1426,7 +1796,7 @@ function Invoke-CasWorkstationOperation { return Invoke-CasOperationPlan -Plan $failedJournal[0].plan -ConfigPath $ConfigPath -Resume -Manifest $Manifest } if (-not $Inventory) { - $Inventory = Get-CasOperationInventory -Profile $Profile -RootPath $RootPath -Manifest $Manifest + $Inventory = Get-CasOperationInventory -Profile $Profile -RootPath $RootPath -ConfigPath $ConfigPath -Manifest $Manifest } $plan = New-CasOperationPlan -Mode $Mode -Profile $Profile -RootPath $RootPath -ConfigPath $ConfigPath -Manifest $Manifest -Inventory $Inventory if (-not $Apply) { diff --git a/stack.manifest.json b/stack.manifest.json index ca4c32f..7465a65 100644 --- a/stack.manifest.json +++ b/stack.manifest.json @@ -11,7 +11,8 @@ "allowedInstallerKinds": ["winget", "scoop", "npm", "manual"], "allowedCommands": ["git", "gh", "node", "npm", "python", "uv", "dotnet", "docker", "az", "wsl.exe", "codex", "claude", "gemini"], "allowedRepositoryPrefixes": ["https://github.com/Coding-Autopilot-System/"], - "allowedConfigTargets": ["codex.mcp.json", "claude.settings.fragment.json", "gemini.mcp.json"] + "allowedConfigTargets": ["codex.mcp.json", "claude.settings.fragment.json", "gemini.mcp.json"], + "allowedAdapters": ["json-mcp", "tree-copy"] }, "profiles": { "core": { @@ -328,32 +329,46 @@ "clients": [ { "id": "codex", - "fileName": "codex.mcp.json" + "fileName": "codex.mcp.json", + "adapter": "json-mcp", + "ownershipKey": "cas-workstation.prompt-refiner" }, { "id": "claude", - "fileName": "claude.settings.fragment.json" + "fileName": "claude.settings.fragment.json", + "adapter": "json-mcp", + "ownershipKey": "cas-workstation.prompt-refiner" }, { "id": "gemini", - "fileName": "gemini.mcp.json" + "fileName": "gemini.mcp.json", + "adapter": "json-mcp", + "ownershipKey": "cas-workstation.prompt-refiner" } ], "skills": [ { "id": "prompt-refiner", - "repo": "Promptimprover" + "repo": "Promptimprover", + "sourceRelativePath": "skills\\prompt-refiner", + "targetRelativePath": "skills\\prompt-refiner", + "adapter": "tree-copy" } ], "workspaces": [ { "id": "cas-default", - "relativePath": "workspaces\\default" + "repo": "Promptimprover", + "sourceRelativePath": "docs", + "targetRelativePath": "workspaces\\default", + "adapter": "tree-copy" } ], "sharedMcpServer": { "name": "prompt-refiner", "transport": "stdio", + "scope": "local-workstation", + "authReference": null, "command": "node", "args": [ "C:\\CodingAutopilotSystem\\repos\\Promptimprover\\dist\\index.js" diff --git a/tests/Apply.Tests.ps1 b/tests/Apply.Tests.ps1 index 7a0194f..a2cdce6 100644 --- a/tests/Apply.Tests.ps1 +++ b/tests/Apply.Tests.ps1 @@ -69,4 +69,34 @@ Describe "CAS journaled plan apply" { { Invoke-CasOperationPlan -Plan $tampered -ConfigPath (Join-Path $script:root tampered) -OperationHandler { param($operation) } } | Should -Throw "*integrity*" } + + It "applies a typed client operation and records owned-content evidence" { + $manifest = Get-CasManifest | ConvertTo-Json -Depth 30 | ConvertFrom-Json + ($manifest.clients | Where-Object id -eq codex).ownershipKey = "cas-workstation.custom-refiner" + $config = Join-Path $script:root client-apply + $client = $manifest.clients | Where-Object id -eq codex + $target = Get-CasClientTarget -Client $client -ConfigPath $config -Manifest $manifest + $operation = [pscustomobject]@{ + id = "client:codex"; kind = "configuration"; resourceCategory = "client"; adapter = "json-mcp" + ownershipKey = $client.ownershipKey; target = $target; risk = "medium"; action = "create" + command = "merge CAS-owned MCP configuration"; source = "manifest:sharedMcpServer"; reason = "missing" + desiredDigest = Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject (Get-CasDesiredMcpNode -Manifest $manifest)) + } + $plan = [pscustomobject]@{ + schemaVersion = "1.0.0"; planId = $null; correlationId = $null; mode = "setup"; profile = "core" + rootPath = $script:root; configPath = $config; desiredStateDigest = "sha256:$('c' * 64)"; operations = @($operation) + } + $identity = [ordered]@{ schemaVersion = $plan.schemaVersion; mode = $plan.mode; profile = $plan.profile; rootPath = $plan.rootPath; configPath = $plan.configPath; desiredStateDigest = $plan.desiredStateDigest; operations = $plan.operations } + $plan.planId = Get-CasSha256 -Value (ConvertTo-CasCanonicalJson -InputObject $identity) + $plan.correlationId = $plan.planId + + $journal = Invoke-CasOperationPlan -Plan $plan -ConfigPath $config -Manifest $manifest + $state = Read-CasManagedState -Path (Get-CasManagedStatePath -ConfigPath $config -Manifest $manifest) + + $journal.status | Should -Be "succeeded" + $state.resources[0].id | Should -Be "client:codex" + $state.resources[0].contentDigest | Should -Be $operation.desiredDigest + (Get-Content -LiteralPath $target -Raw | ConvertFrom-Json).mcpServers.'cas-workstation.custom-refiner' | Should -Not -BeNullOrEmpty + (Get-Content -LiteralPath $target -Raw | ConvertFrom-Json).mcpServers.PSObject.Properties["cas-workstation.prompt-refiner"] | Should -BeNullOrEmpty + } } diff --git a/tests/ClientConfig.Tests.ps1 b/tests/ClientConfig.Tests.ps1 new file mode 100644 index 0000000..d2f58d9 --- /dev/null +++ b/tests/ClientConfig.Tests.ps1 @@ -0,0 +1,41 @@ +BeforeAll { + $script:repoRoot = Split-Path -Parent $PSScriptRoot + Import-Module (Join-Path $script:repoRoot "scripts\Cas.Workstation.psm1") -Force + $script:manifest = Get-CasManifest + $script:client = $script:manifest.clients | Where-Object id -eq "codex" +} + +Describe "CAS owned client configuration" { + It "merges only the CAS-owned namespace and preserves unrelated settings" { + $existing = '{"theme":"dark","mcpServers":{"user.server":{"command":"user"}}}' | ConvertFrom-Json + $merged = Merge-CasClientConfiguration -ExistingConfiguration $existing -Client $script:client -Manifest $script:manifest + + $merged.theme | Should -Be "dark" + $merged.mcpServers.'user.server'.command | Should -Be "user" + $merged.mcpServers.'cas-workstation.prompt-refiner'.scope | Should -Be "local-workstation" + } + + It "removes only the CAS-owned namespace" { + $existing = Merge-CasClientConfiguration -ExistingConfiguration ('{"mcpServers":{"user.server":{"command":"user"}}}' | ConvertFrom-Json) -Client $script:client -Manifest $script:manifest + $updated = Remove-CasClientConfiguration -ExistingConfiguration $existing -OwnershipKey $script:client.ownershipKey + + $updated.mcpServers.'user.server'.command | Should -Be "user" + $updated.mcpServers.PSObject.Properties[$script:client.ownershipKey] | Should -BeNullOrEmpty + } + + It "atomically applies and reports owned-content drift without reacting to unrelated changes" { + $config = Join-Path $TestDrive config + New-Item -ItemType Directory -Path $config | Out-Null + $result = Set-CasClientConfiguration -Client $script:client -ConfigPath $config -ApprovedRoots $config -Manifest $script:manifest + $status = Get-CasClientConfigurationStatus -Client $script:client -ConfigPath $config -Manifest $script:manifest + + $result.contentDigest | Should -Match "^sha256:" + $status.status | Should -Be "satisfied" + + $target = Get-CasClientTarget -Client $script:client -ConfigPath $config -Manifest $script:manifest + $document = Get-Content $target -Raw | ConvertFrom-Json + $document | Add-Member -MemberType NoteProperty -Name theme -Value dark + $document | ConvertTo-Json -Depth 20 | Set-Content $target + (Get-CasClientConfigurationStatus -Client $script:client -ConfigPath $config -Manifest $script:manifest).status | Should -Be "satisfied" + } +} diff --git a/tests/ManagedTrees.Tests.ps1 b/tests/ManagedTrees.Tests.ps1 new file mode 100644 index 0000000..e521cae --- /dev/null +++ b/tests/ManagedTrees.Tests.ps1 @@ -0,0 +1,44 @@ +BeforeAll { + $script:repoRoot = Split-Path -Parent $PSScriptRoot + Import-Module (Join-Path $script:repoRoot "scripts\Cas.Workstation.psm1") -Force +} + +Describe "CAS deterministic managed trees" { + It "produces stable digests independent of file creation order" { + $one = Join-Path $TestDrive one + $two = Join-Path $TestDrive two + New-Item -ItemType Directory -Path $one,$two | Out-Null + "a" | Set-Content (Join-Path $one a.txt) + "b" | Set-Content (Join-Path $one b.txt) + "b" | Set-Content (Join-Path $two b.txt) + "a" | Set-Content (Join-Path $two a.txt) + + (Get-CasTreeDigest -Path $one -ApprovedRoots $TestDrive) | Should -BeExactly (Get-CasTreeDigest -Path $two -ApprovedRoots $TestDrive) + } + + It "copies an absent managed tree and rejects adoption of an existing target" { + $source = Join-Path $TestDrive source + $target = Join-Path $TestDrive target + New-Item -ItemType Directory -Path $source | Out-Null + "content" | Set-Content (Join-Path $source file.txt) + + $result = Copy-CasManagedTree -Source $source -Target $target -ApprovedRoots $TestDrive + $result.contentDigest | Should -Be (Get-CasTreeDigest -Path $source -ApprovedRoots $TestDrive) + { Copy-CasManagedTree -Source $source -Target $target -ApprovedRoots $TestDrive } | Should -Throw "*cannot be adopted*" + } + + It "removes obsolete owned files during a digest-proven update" { + $source = Join-Path $TestDrive update-source + $target = Join-Path $TestDrive update-target + New-Item -ItemType Directory -Path $source | Out-Null + "keep" | Set-Content (Join-Path $source keep.txt) + "obsolete" | Set-Content (Join-Path $source obsolete.txt) + $null = Copy-CasManagedTree -Source $source -Target $target -ApprovedRoots $TestDrive + $priorDigest = Get-CasTreeDigest -Path $target -ApprovedRoots $TestDrive + Remove-Item (Join-Path $source obsolete.txt) + + $null = Copy-CasManagedTree -Source $source -Target $target -ApprovedRoots $TestDrive -ReplaceOwned -ExpectedOwnedDigest $priorDigest + Test-Path (Join-Path $target obsolete.txt) | Should -BeFalse + (Get-CasTreeDigest -Path $target -ApprovedRoots $TestDrive) | Should -Be (Get-CasTreeDigest -Path $source -ApprovedRoots $TestDrive) + } +} diff --git a/tests/Manifest.Tests.ps1 b/tests/Manifest.Tests.ps1 index 08583b7..675d841 100644 --- a/tests/Manifest.Tests.ps1 +++ b/tests/Manifest.Tests.ps1 @@ -31,6 +31,22 @@ Describe "CAS manifest validation and resolution" { { Assert-CasManifest -Manifest $manifest } | Should -Throw "*unallowlisted URL*" } + It "rejects unsafe managed-tree sources and secret-like MCP auth values" { + $manifest = Get-Content $script:manifestPath -Raw | ConvertFrom-Json + $manifest.skills[0].sourceRelativePath = "..\escape" + { Assert-CasManifest -Manifest $manifest } | Should -Throw "*unsafe relative path*" + + $manifest = Get-Content $script:manifestPath -Raw | ConvertFrom-Json + $manifest.sharedMcpServer.authReference = "Bearer real-token" + { Assert-CasManifest -Manifest $manifest } | Should -Throw "*environment reference*" + } + + It "keeps local and production MCP transport boundaries explicit" { + $manifest = Get-Content $script:manifestPath -Raw | ConvertFrom-Json + $manifest.sharedMcpServer.transport = "http" + { Assert-CasManifest -Manifest $manifest } | Should -Throw "*Local workstation MCP servers must use stdio*" + } + It "resolves every declarative category with explicit requirement level" { $resolved = Resolve-CasDesiredState -Profile core @($resolved.desiredState.resources.category | Sort-Object -Unique) | Should -Be @("clients", "repos", "services", "skills", "tools", "workspaces") diff --git a/tests/OperationWorkflow.Tests.ps1 b/tests/OperationWorkflow.Tests.ps1 index d861447..54e77fd 100644 --- a/tests/OperationWorkflow.Tests.ps1 +++ b/tests/OperationWorkflow.Tests.ps1 @@ -29,6 +29,12 @@ Describe "CAS shared operational workflow" { } } + It "plans clean setup before selected managed-tree source repositories exist" { + $inventory = Get-CasOperationInventory -Profile full -RootPath $script:root -ConfigPath $script:config -Manifest $script:manifest + { New-CasOperationPlan -Mode setup -Profile full -RootPath $script:root -ConfigPath $script:config -Inventory $inventory -Manifest $script:manifest } | Should -Not -Throw + ($inventory.resources | Where-Object id -eq "skill:prompt-refiner").status | Should -Be "pending-source" + } + It "resumes the latest persisted failed plan through the shared workflow" { $config = Join-Path $script:root recovery $failed = Invoke-CasWorkstationOperation -Mode repair -Profile core -RootPath $script:root -ConfigPath $config -Inventory $script:inventory -Manifest $script:manifest -Apply -OperationHandler { param($operation) throw "synthetic" } diff --git a/tests/Plan.Tests.ps1 b/tests/Plan.Tests.ps1 index f66d132..86bebe0 100644 --- a/tests/Plan.Tests.ps1 +++ b/tests/Plan.Tests.ps1 @@ -13,7 +13,10 @@ Describe "CAS deterministic operation planning" { (ConvertTo-CasCanonicalJson $first) | Should -BeExactly (ConvertTo-CasCanonicalJson $second) $first.planId | Should -Match "^sha256:[a-f0-9]{64}$" - $first.operations.id | Should -Be ($first.operations.id | Sort-Object) + $kinds = @($first.operations.kind) + $kinds.IndexOf("repository") | Should -BeGreaterThan $kinds.IndexOf("tool") + $kinds.IndexOf("directory") | Should -BeGreaterThan $kinds.IndexOf("repository") + $kinds.IndexOf("configuration") | Should -BeGreaterThan $kinds.IndexOf("directory") } It "shows commands sources risks and changes before apply" { @@ -41,4 +44,12 @@ Describe "CAS deterministic operation planning" { ($plan.operations | Where-Object id -eq "repo:autogen").action | Should -Be "update" } + + It "plans selected clients skills and workspaces as typed operations" { + $plan = New-CasOperationPlan -Mode setup -Profile full -RootPath $script:root -ConfigPath $script:config -Inventory ([pscustomobject]@{ resources = @() }) + + ($plan.operations | Where-Object id -eq "client:codex").kind | Should -Be "configuration" + ($plan.operations | Where-Object id -eq "skill:prompt-refiner").adapter | Should -Be "tree-copy" + ($plan.operations | Where-Object id -eq "workspace:cas-default").resourceCategory | Should -Be "workspace" + } } diff --git a/tests/Uninstall.Tests.ps1 b/tests/Uninstall.Tests.ps1 index 18fddca..c151f66 100644 --- a/tests/Uninstall.Tests.ps1 +++ b/tests/Uninstall.Tests.ps1 @@ -89,4 +89,39 @@ Describe "CAS ledger-only uninstall" { (Get-Content -LiteralPath $target -Raw | ConvertFrom-Json).user | Should -BeTrue } + + It "surgically removes client-owned configuration while preserving later user changes" { + $manifest = Get-CasManifest + $client = $manifest.clients | Where-Object id -eq codex + $target = Get-CasClientTarget -Client $client -ConfigPath $script:config -Manifest $manifest + New-Item -ItemType Directory -Path (Split-Path -Parent $target) -Force | Out-Null + $document = Merge-CasClientConfiguration -ExistingConfiguration ('{"theme":"dark","mcpServers":{"user.server":{"command":"user"}}}' | ConvertFrom-Json) -Client $client -Manifest $manifest + $document | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $target + $backup = Join-Path $script:config "client.backup.json" + '{}' | Set-Content -LiteralPath $backup + $null = Add-CasManagedResource -State $script:state -Id "client:codex" -Kind configuration -Ownership modified -Target $target -WasPresentBefore $true -BackupTarget $backup + Write-CasManagedState -State $script:state -Path $script:statePath -ApprovedRoots $script:config + + $preview = Get-CasUninstallPreview -StatePath $script:statePath -ApprovedRoots $script:config + $preview.actions[0].action | Should -Be "remove-owned-configuration" + $null = Invoke-CasUninstall -Preview $preview -ApprovedRoots $script:config -Confirm:$false + $updated = Get-Content -LiteralPath $target -Raw | ConvertFrom-Json + $updated.theme | Should -Be "dark" + $updated.mcpServers.'user.server'.command | Should -Be "user" + $updated.mcpServers.PSObject.Properties[$client.ownershipKey] | Should -BeNullOrEmpty + } + + It "removes a nonempty managed tree only when its ledger digest still matches" { + $tree = Join-Path $script:config "skills\owned" + New-Item -ItemType Directory -Path $tree -Force | Out-Null + "owned" | Set-Content (Join-Path $tree SKILL.md) + $digest = Get-CasTreeDigest -Path $tree -ApprovedRoots $script:config + $null = Add-CasManagedResource -State $script:state -Id "skill:owned" -Kind directory -Ownership created -Target $tree -WasPresentBefore $false -ContentDigest $digest + Write-CasManagedState -State $script:state -Path $script:statePath -ApprovedRoots $script:config + + $preview = Get-CasUninstallPreview -StatePath $script:statePath -ApprovedRoots $script:config + $preview.actions[0].action | Should -Be "remove-owned-tree" + $null = Invoke-CasUninstall -Preview $preview -ApprovedRoots $script:config -Confirm:$false + Test-Path $tree | Should -BeFalse + } } diff --git a/tests/fixtures/contracts/manifest.valid.json b/tests/fixtures/contracts/manifest.valid.json index ba52176..f68aca2 100644 --- a/tests/fixtures/contracts/manifest.valid.json +++ b/tests/fixtures/contracts/manifest.valid.json @@ -1 +1 @@ -{"manifestVersion":"1.0.0","bundleName":"CAS Workstation","bundleId":"cas-workstation","defaults":{"rootPath":"C:\\CAS","configPath":"C:\\Users\\developer\\.cas","profile":"core"},"policy":{"allowedInstallerKinds":["winget"],"allowedCommands":["node"],"allowedRepositoryPrefixes":["https://github.com/example/"],"allowedConfigTargets":["client.json"]},"profiles":{"core":{"description":"Core","tools":{"required":[],"optional":[]},"repos":{"required":[],"optional":[]},"services":{"required":[],"optional":[]},"clients":{"required":[],"optional":[]},"skills":{"required":[],"optional":[]},"workspaces":{"required":[],"optional":[]}}},"paths":{"state":"state"},"tools":[],"repos":[],"services":[],"clients":[],"skills":[],"workspaces":[],"sharedMcpServer":{"name":"refiner","transport":"stdio","command":"node","args":[]}} +{"manifestVersion":"1.0.0","bundleName":"CAS Workstation","bundleId":"cas-workstation","defaults":{"rootPath":"C:\\CAS","configPath":"C:\\Users\\developer\\.cas","profile":"core"},"policy":{"allowedInstallerKinds":["winget"],"allowedCommands":["node"],"allowedRepositoryPrefixes":["https://github.com/example/"],"allowedConfigTargets":["client.json"],"allowedAdapters":["json-mcp","tree-copy"]},"profiles":{"core":{"description":"Core","tools":{"required":[],"optional":[]},"repos":{"required":["repo-1"],"optional":[]},"services":{"required":[],"optional":[]},"clients":{"required":["client-1"],"optional":[]},"skills":{"required":["skill-1"],"optional":[]},"workspaces":{"required":["workspace-1"],"optional":[]}}},"paths":{"state":"state"},"tools":[],"repos":[{"id":"repo-1","url":"https://github.com/example/repo-1.git","defaultBranch":"main"}],"services":[],"clients":[{"id":"client-1","fileName":"client.json","adapter":"json-mcp","ownershipKey":"cas-workstation.refiner"}],"skills":[{"id":"skill-1","repo":"repo-1","sourceRelativePath":"skills\\one","targetRelativePath":"skills\\one","adapter":"tree-copy"}],"workspaces":[{"id":"workspace-1","repo":"repo-1","sourceRelativePath":"workspace","targetRelativePath":"workspaces\\one","adapter":"tree-copy"}],"sharedMcpServer":{"name":"refiner","transport":"stdio","scope":"local-workstation","authReference":null,"command":"node","args":[]}}