Wave 0 + Wave 1 core of the transparent agentic-client integration: a
developer types prompts normally in Claude Code / Codex while every
certified turn still carries a full H1->H7 trace and an H6 record.
- agentic_bridge.py: stdlib-only lifecycle state machine (begin/pre-tool/
post-tool/telemetry/finalize/abort + report/doctor). Single-model
invariant (never calls a model), fail-closed at the side-effect point,
admission TTL + canonical-project/session binding, atomic state under
.specify/state/agentic-sessions/, secret redaction, null-not-zero H6.
- agentic-lifecycle.schema.json: client-agnostic JSON contract.
- adapters/claude-code + adapters/codex: thin hook renderers + config
templates that call the core bridge.
- phase-agentic-bridge-tests.sh: C1-C12 acceptance + threat suite (30/30).
- devkit templates/{claude,codex} + windows/install-agentic.ps1
(install/doctor/uninstall with manifest, path-safe).
- docs/casan Windows + security/bypass guides; plan status -> IMPLEMENTED.
- harden generate-agentops-dashboard.py aggregation against null H6 costs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
205 lines
8.5 KiB
PowerShell
205 lines
8.5 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
CASAN Plan-20 agentic-client installer for Windows PowerShell.
|
|
|
|
.DESCRIPTION
|
|
Installs, verifies (doctor) or removes the CASAN agentic-client integration
|
|
(Claude Code and/or Codex project hooks) in a target repository — from a
|
|
clean clone, with NO manual file copying. Every created file is recorded in a
|
|
manifest so uninstall never touches a user's own config.
|
|
|
|
The hooks call the CASAN agentic bridge, which never runs a model (Claude/
|
|
Codex remains the sole model executor). See:
|
|
docs/casan/CASAN_AGENTIC_CLIENTS_WINDOWS.md
|
|
docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md
|
|
|
|
.PARAMETER Action
|
|
install | doctor | uninstall (default: install)
|
|
|
|
.PARAMETER Client
|
|
claude | codex | all (default: all)
|
|
|
|
.PARAMETER Target
|
|
Target repository root. Default: the repo this script lives in.
|
|
|
|
.PARAMETER Mode
|
|
observe | enforce (default: observe) — sets the bridge
|
|
enforcement mode written into the target's .casan/agentic.env.
|
|
|
|
.EXAMPLE
|
|
pwsh packages/casan-devkit/windows/install-agentic.ps1 -Client claude -Mode enforce
|
|
|
|
.EXAMPLE
|
|
pwsh packages/casan-devkit/windows/install-agentic.ps1 -Action doctor -Client all
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet('install', 'doctor', 'uninstall')] [string]$Action = 'install',
|
|
[ValidateSet('claude', 'codex', 'all')] [string]$Client = 'all',
|
|
[string]$Target,
|
|
[ValidateSet('observe', 'enforce')] [string]$Mode = 'observe'
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# ── Resolve roots ────────────────────────────────────────────────────────────
|
|
# This script lives at <repo>/packages/casan-devkit/windows/install-agentic.ps1
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..\..\..')).Path
|
|
if (-not $Target) { $Target = $RepoRoot }
|
|
$Target = (Resolve-Path $Target).Path
|
|
|
|
$HarnessRel = 'packages\casan-harness'
|
|
$Bridge = Join-Path $Target "$HarnessRel\scripts\python\agentic_bridge.py"
|
|
$ClaudeTpl = Join-Path $Target "$HarnessRel\adapters\claude-code\settings.template.json"
|
|
$CodexHooks = Join-Path $Target "$HarnessRel\adapters\codex\hooks.template.json"
|
|
$CodexConf = Join-Path $Target "$HarnessRel\adapters\codex\config.template.toml"
|
|
$ManifestDir = Join-Path $Target '.casan'
|
|
$Manifest = Join-Path $ManifestDir 'agentic-install-manifest.json'
|
|
|
|
function Resolve-Python {
|
|
foreach ($cand in @('python3', 'python', 'py')) {
|
|
$cmd = Get-Command $cand -ErrorAction SilentlyContinue
|
|
if ($cmd) {
|
|
try { & $cmd.Source --version *> $null; if ($LASTEXITCODE -eq 0) { return $cmd.Source } } catch {}
|
|
}
|
|
}
|
|
throw 'No working Python 3 interpreter found on PATH (need python3/python/py).'
|
|
}
|
|
|
|
function Write-Info($m) { Write-Host "[casan] $m" }
|
|
function Write-Ok($m) { Write-Host "[casan] OK $m" -ForegroundColor Green }
|
|
function Write-Warn2($m) { Write-Host "[casan] !! $m" -ForegroundColor Yellow }
|
|
function Write-Err($m) { Write-Host "[casan] ERR $m" -ForegroundColor Red }
|
|
|
|
function Load-Manifest {
|
|
if (Test-Path $Manifest) {
|
|
try { return Get-Content -Raw -Path $Manifest | ConvertFrom-Json } catch {}
|
|
}
|
|
return [PSCustomObject]@{ created = @(); clients = @() }
|
|
}
|
|
|
|
function Save-Manifest($m) {
|
|
if (-not (Test-Path $ManifestDir)) { New-Item -ItemType Directory -Force -Path $ManifestDir | Out-Null }
|
|
$m | ConvertTo-Json -Depth 6 | Set-Content -Path $Manifest -Encoding UTF8
|
|
}
|
|
|
|
# Copy a template into place. Never overwrite a user's existing config silently:
|
|
# back it up first and record both in the manifest so uninstall can restore it.
|
|
function Install-File($src, $dst, $manifest) {
|
|
$dstDir = Split-Path -Parent $dst
|
|
if (-not (Test-Path $dstDir)) { New-Item -ItemType Directory -Force -Path $dstDir | Out-Null }
|
|
if (Test-Path $dst) {
|
|
$backup = "$dst.casan-bak"
|
|
if (-not (Test-Path $backup)) {
|
|
Copy-Item -Path $dst -Destination $backup -Force
|
|
Write-Warn2 "existing $([System.IO.Path]::GetFileName($dst)) backed up to $([System.IO.Path]::GetFileName($backup))"
|
|
}
|
|
}
|
|
Copy-Item -Path $src -Destination $dst -Force
|
|
$entry = @{ path = $dst; from = $src }
|
|
$manifest.created = @($manifest.created + $dst | Select-Object -Unique)
|
|
Write-Ok "installed $dst"
|
|
}
|
|
|
|
function Do-Install {
|
|
if (-not (Test-Path $Bridge)) { throw "Bridge not found at $Bridge — is this a CASAN repo?" }
|
|
$py = Resolve-Python
|
|
$manifest = Load-Manifest
|
|
$clients = @()
|
|
|
|
if ($Client -in @('claude', 'all')) {
|
|
Install-File $ClaudeTpl (Join-Path $Target '.claude\settings.json') $manifest
|
|
$clients += 'claude'
|
|
}
|
|
if ($Client -in @('codex', 'all')) {
|
|
Install-File $CodexHooks (Join-Path $Target '.codex\hooks.json') $manifest
|
|
Install-File $CodexConf (Join-Path $Target '.codex\config.toml') $manifest
|
|
$clients += 'codex'
|
|
Write-Warn2 'Codex loads project hooks only AFTER you accept its trust prompt — open the repo in Codex once to complete onboarding.'
|
|
}
|
|
|
|
# Bridge feature flags for the target (sourced by the user's shell/session).
|
|
$envFile = Join-Path $Target '.casan\agentic.env'
|
|
@(
|
|
"# CASAN Plan-20 agentic bridge flags (Windows). Source before starting the client.",
|
|
"CASAN_AGENTIC_BRIDGE_ENABLED=1",
|
|
"CASAN_AGENTIC_ENFORCEMENT_MODE=$Mode",
|
|
"CASAN_AGENTIC_INTEGRATION_MODE=project_hook"
|
|
) | Set-Content -Path $envFile -Encoding UTF8
|
|
$manifest.created = @($manifest.created + $envFile | Select-Object -Unique)
|
|
Write-Ok "wrote flags -> $envFile (mode=$Mode)"
|
|
|
|
$manifest.clients = @($clients | Select-Object -Unique)
|
|
Save-Manifest $manifest
|
|
|
|
Write-Info 'Running doctor to verify...'
|
|
Do-Doctor
|
|
Write-Info "Install complete. Set CASAN_AGENTIC_ENFORCEMENT_MODE=enforce when ready to certify turns."
|
|
}
|
|
|
|
function Do-Doctor {
|
|
$py = Resolve-Python
|
|
Write-Info "python : $py"
|
|
Write-Info "target : $Target"
|
|
Write-Info "bridge : $Bridge"
|
|
if (-not (Test-Path $Bridge)) { Write-Err 'bridge missing'; exit 1 }
|
|
|
|
# Core bridge self-diagnostics.
|
|
& $py $Bridge doctor
|
|
if ($LASTEXITCODE -ne 0) { Write-Err 'bridge doctor reported a problem'; }
|
|
|
|
$ok = $true
|
|
if ($Client -in @('claude', 'all')) {
|
|
if (Test-Path (Join-Path $Target '.claude\settings.json')) { Write-Ok '.claude\settings.json present' }
|
|
else { Write-Warn2 '.claude\settings.json missing (run install)'; $ok = $false }
|
|
}
|
|
if ($Client -in @('codex', 'all')) {
|
|
if (Test-Path (Join-Path $Target '.codex\hooks.json')) { Write-Ok '.codex\hooks.json present (Codex trust review still required)' }
|
|
else { Write-Warn2 '.codex\hooks.json missing (run install)'; $ok = $false }
|
|
}
|
|
|
|
# A real begin->finalize smoke turn against an isolated state dir.
|
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("casan-agentic-" + [guid]::NewGuid().ToString('N'))
|
|
$env:CASAN_STATE_ROOT = (Join-Path $tmp '.specify')
|
|
try {
|
|
$begin = '{"op":"begin","client":"claude-code","project":"' + ($Target -replace '\\','/') + '","session":"doctor","prompt":"doctor smoke","integration_mode":"project_hook"}'
|
|
$resp = $begin | & $py $Bridge run | ConvertFrom-Json
|
|
if ($resp.decision -eq 'allow') { Write-Ok "smoke begin admitted (strength=$($resp.certification_strength))" }
|
|
else { Write-Warn2 "smoke begin decision=$($resp.decision)"; }
|
|
} finally {
|
|
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
|
Remove-Item Env:\CASAN_STATE_ROOT -ErrorAction SilentlyContinue
|
|
}
|
|
if ($ok) { Write-Ok 'doctor passed' } else { Write-Warn2 'doctor found missing config' }
|
|
}
|
|
|
|
function Do-Uninstall {
|
|
$manifest = Load-Manifest
|
|
if (-not $manifest.created -or $manifest.created.Count -eq 0) {
|
|
Write-Warn2 'no install manifest found — nothing to remove'
|
|
return
|
|
}
|
|
foreach ($f in $manifest.created) {
|
|
if (Test-Path $f) {
|
|
Remove-Item -Force $f
|
|
Write-Ok "removed $f"
|
|
}
|
|
$backup = "$f.casan-bak"
|
|
if (Test-Path $backup) {
|
|
Move-Item -Force $backup $f
|
|
Write-Ok "restored user's original $f from backup"
|
|
}
|
|
}
|
|
Remove-Item -Force $Manifest -ErrorAction SilentlyContinue
|
|
Write-Info 'Uninstall complete — your own (non-CASAN) config was preserved.'
|
|
}
|
|
|
|
switch ($Action) {
|
|
'install' { Do-Install }
|
|
'doctor' { Do-Doctor }
|
|
'uninstall' { Do-Uninstall }
|
|
}
|