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.
+
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.
+
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.
+
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":[]}}