@caddis/cli 0.4.13 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundles/antigravity-plugin/caddis_statusline.py +30 -3
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/repo_freshness.py +146 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.ps1 +11 -0
- package/bundles/antigravity-plugin/scripts/caddis-run.sh +9 -0
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +24 -3
- package/bundles/antigravity-plugin/scripts/caddis_governor.py +542 -0
- package/bundles/antigravity-plugin/scripts/caddis_host_guard.py +689 -0
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +1632 -0
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +30 -3
- package/bundles/antigravity-plugin/scripts/install-host-guard.ps1 +393 -0
- package/bundles/antigravity-plugin/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/SKILL.md +375 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
- package/bundles/antigravity-plugin/skills/confluence-hla/scripts/discover_db_lineage.py +970 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +30 -4
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +48 -1
- package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +155 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +38 -4
- package/bundles/codex/.codex/skills/docs/confluence-hla/SKILL.md +375 -0
- package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/convert_hla_to_confluence.py +1164 -0
- package/bundles/codex/.codex/skills/docs/confluence-hla/scripts/discover_db_lineage.py +970 -0
- package/bundles/manifest.json +2 -2
- package/dist/claude-deepseek.js +50 -10
- package/dist/claude-glm.js +50 -10
- package/dist/claude-oss.js +50 -10
- package/dist/cli.js +173 -63
- package/package.json +1 -1
|
@@ -196,7 +196,7 @@ def git_facts(cwd: str) -> dict:
|
|
|
196
196
|
try:
|
|
197
197
|
proc = subprocess.run(
|
|
198
198
|
["git", "-C", cwd, "--no-optional-locks", "status", "--porcelain=v2", "--branch"],
|
|
199
|
-
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=
|
|
199
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=1.0,
|
|
200
200
|
)
|
|
201
201
|
text = proc.stdout.decode("utf-8", "replace")
|
|
202
202
|
except Exception:
|
|
@@ -582,6 +582,7 @@ def render_agy(data: dict, cfg: dict) -> str:
|
|
|
582
582
|
used = as_int(cw.get("total_input_tokens"))
|
|
583
583
|
max_tokens = as_int(cw.get("context_window_size")) or 1_048_576
|
|
584
584
|
pct = float(as_int(cw.get("used_percentage")))
|
|
585
|
+
record_context(cwd, pct)
|
|
585
586
|
parts.append(context_segment(c, cfg, used, max_tokens, pct))
|
|
586
587
|
loud = cached_segment(c, cfg, cached)
|
|
587
588
|
if loud:
|
|
@@ -821,6 +822,10 @@ def main(argv=None) -> int:
|
|
|
821
822
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
822
823
|
except Exception:
|
|
823
824
|
pass
|
|
825
|
+
try:
|
|
826
|
+
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
|
|
827
|
+
except Exception:
|
|
828
|
+
pass
|
|
824
829
|
|
|
825
830
|
if args.install:
|
|
826
831
|
hosts = ["claude", "agy"] if args.host == "both" else [args.host]
|
|
@@ -854,9 +859,31 @@ def main(argv=None) -> int:
|
|
|
854
859
|
# A status line must never break the host. Degrade to the directory name.
|
|
855
860
|
line = os.path.basename(os.getcwd())
|
|
856
861
|
|
|
857
|
-
|
|
862
|
+
try:
|
|
863
|
+
print(line)
|
|
864
|
+
sys.stdout.flush()
|
|
865
|
+
except (BrokenPipeError, OSError):
|
|
866
|
+
# Parent process cancelled or closed stdout pipe (e.g. agy runner on user prompt/state switch).
|
|
867
|
+
# Redirecting to devnull prevents Windows Python from throwing unhandled TextIOWrapper errors on exit.
|
|
868
|
+
try:
|
|
869
|
+
devnull = open(os.devnull, "w")
|
|
870
|
+
os.dup2(devnull.fileno(), sys.stdout.fileno())
|
|
871
|
+
except Exception:
|
|
872
|
+
pass
|
|
873
|
+
return 0
|
|
874
|
+
except Exception:
|
|
875
|
+
return 0
|
|
876
|
+
|
|
858
877
|
return 0
|
|
859
878
|
|
|
860
879
|
|
|
861
880
|
if __name__ == "__main__":
|
|
862
|
-
|
|
881
|
+
try:
|
|
882
|
+
code = main()
|
|
883
|
+
except BaseException:
|
|
884
|
+
code = 0
|
|
885
|
+
try:
|
|
886
|
+
sys.stdout.flush()
|
|
887
|
+
except Exception:
|
|
888
|
+
pass
|
|
889
|
+
sys.exit(code)
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[switch]$Apply,
|
|
4
|
+
[string]$NssmSha256,
|
|
5
|
+
[ValidateSet('report', 'enforce')]
|
|
6
|
+
[string]$Mode = 'report'
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
Set-StrictMode -Version Latest
|
|
10
|
+
$ErrorActionPreference = 'Stop'
|
|
11
|
+
|
|
12
|
+
$sourceNssm = 'C:\Tools\nssm\nssm.exe'
|
|
13
|
+
$serviceName = 'caddis-host-guard'
|
|
14
|
+
$programRoot = Join-Path $env:ProgramFiles 'caddis'
|
|
15
|
+
$nssmDir = Join-Path $programRoot 'nssm'
|
|
16
|
+
$nssm = Join-Path $nssmDir 'nssm.exe'
|
|
17
|
+
$pythonDir = Join-Path $env:ProgramFiles 'caddis\python'
|
|
18
|
+
$privatePython = Join-Path $pythonDir 'python.exe'
|
|
19
|
+
$serviceDir = Join-Path $env:ProgramData 'caddis'
|
|
20
|
+
$guardScript = Join-Path $serviceDir 'caddis_host_guard.py'
|
|
21
|
+
$governorScript = Join-Path $serviceDir 'caddis_governor.py'
|
|
22
|
+
$budget = Join-Path $env:ProgramData 'caddis\host-budget.toml'
|
|
23
|
+
$stdoutLog = Join-Path $serviceDir 'guard.out.log'
|
|
24
|
+
$stderrLog = Join-Path $serviceDir 'guard.err.log'
|
|
25
|
+
$sourceGuard = Join-Path $PSScriptRoot 'caddis_host_guard.py'
|
|
26
|
+
$sourceGovernor = Join-Path $PSScriptRoot 'caddis_governor.py'
|
|
27
|
+
|
|
28
|
+
if ($Apply -and $NssmSha256 -notmatch '\A[0-9a-fA-F]{64}\z') {
|
|
29
|
+
[Console]::Error.WriteLine('-Apply requires -NssmSha256 with the trusted NSSM SHA256 (64 hexadecimal characters).')
|
|
30
|
+
exit 3
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (-not (Test-Path -LiteralPath $sourceNssm -PathType Leaf)) {
|
|
34
|
+
[Console]::Error.WriteLine("NSSM was not found at $sourceNssm.")
|
|
35
|
+
exit 3
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (-not $Apply) {
|
|
39
|
+
Write-Output '# Dry run. Run again with -Apply -NssmSha256 <trusted SHA256> from an elevated shell.'
|
|
40
|
+
Write-Output "New-Item -ItemType Directory -Force -Path `"$programRoot`""
|
|
41
|
+
Write-Output "New-Item -ItemType Directory -Force -Path `"$serviceDir`""
|
|
42
|
+
Write-Output "Get-ChildItem -LiteralPath `"$programRoot`" -Force -Recurse # refuse reparse points"
|
|
43
|
+
Write-Output "Get-ChildItem -LiteralPath `"$serviceDir`" -Force -Recurse # refuse reparse points"
|
|
44
|
+
Write-Output "icacls `"$programRoot`" /setowner `"*S-1-5-32-544`" /T /C"
|
|
45
|
+
Write-Output "icacls `"$programRoot`" /reset /T /C"
|
|
46
|
+
Write-Output "icacls `"$programRoot`" /inheritance:r /grant:r `"*S-1-5-32-544:(OI)(CI)F`" `"*S-1-5-18:(OI)(CI)F`" `"*S-1-5-32-545:(OI)(CI)RX`""
|
|
47
|
+
Write-Output "icacls `"$serviceDir`" /setowner `"*S-1-5-32-544`" /T /C"
|
|
48
|
+
Write-Output "icacls `"$serviceDir`" /reset /T /C"
|
|
49
|
+
Write-Output "icacls `"$serviceDir`" /inheritance:r /grant:r `"*S-1-5-32-544:(OI)(CI)F`" `"*S-1-5-18:(OI)(CI)F`" `"*S-1-5-32-545:(OI)(CI)RX`""
|
|
50
|
+
Write-Output "New-Item -ItemType Directory -Force -Path `"$nssmDir`""
|
|
51
|
+
Write-Output "Lock-CaddisTree -Path `"$nssmDir`""
|
|
52
|
+
Write-Output "Copy-Item -LiteralPath `"$sourceNssm`" -Destination `"$nssm`" -Force"
|
|
53
|
+
Write-Output "Lock-CaddisTree -Path `"$nssmDir`""
|
|
54
|
+
Write-Output "Get-TrustedFileHash -Path `"$nssm`" # must equal -NssmSha256; refuse with exit 3 otherwise"
|
|
55
|
+
Write-Output '$sourcePython = (Get-Command python -CommandType Application -ErrorAction Stop).Source # require a valid PSF signature'
|
|
56
|
+
Write-Output '$basePrefix = Split-Path -Parent $sourcePython # refuse venvs, python*._pth and python*.zip'
|
|
57
|
+
Write-Output ('robocopy "$basePrefix" "{0}" /MIR /XD Lib\site-packages /XF python*._pth python*.zip' -f $pythonDir)
|
|
58
|
+
Write-Output ('& "{0}" -I -S -c ''import sys; print(sys.base_prefix)'' # must resolve to the private copy' -f $privatePython)
|
|
59
|
+
Write-Output "Copy-Item -LiteralPath `"$sourceGuard`" -Destination `"$guardScript`" -Force"
|
|
60
|
+
Write-Output "Copy-Item -LiteralPath `"$sourceGovernor`" -Destination `"$governorScript`" -Force"
|
|
61
|
+
Write-Output "Write default host budget to `"$budget`" with guard mode `"$Mode`" if it is missing"
|
|
62
|
+
Write-Output "icacls `"$programRoot`" /setowner `"*S-1-5-32-544`" /T /C"
|
|
63
|
+
Write-Output "icacls `"$serviceDir`" /setowner `"*S-1-5-32-544`" /T /C"
|
|
64
|
+
Write-Output "& `"$nssm`" install $serviceName `"$privatePython`" -I -S `"$guardScript`" run --budget `"$budget`""
|
|
65
|
+
Write-Output "Update the existing $serviceName service PathName to the quoted protected path `"$nssm`" using Win32_Service.Change."
|
|
66
|
+
Write-Output "& `"$nssm`" set $serviceName AppStdout `"$stdoutLog`""
|
|
67
|
+
Write-Output "& `"$nssm`" set $serviceName AppStderr `"$stderrLog`""
|
|
68
|
+
Write-Output "& `"$nssm`" set $serviceName ObjectName LocalSystem"
|
|
69
|
+
Write-Output "& `"$nssm`" set $serviceName Start SERVICE_AUTO_START"
|
|
70
|
+
Write-Output "& `"$nssm`" start $serviceName"
|
|
71
|
+
exit 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
|
75
|
+
[Security.Principal.WindowsBuiltInRole]::Administrator
|
|
76
|
+
)
|
|
77
|
+
if (-not $isAdmin) {
|
|
78
|
+
[Console]::Error.WriteLine('install-host-guard.ps1 -Apply requires an elevated shell.')
|
|
79
|
+
exit 3
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
foreach ($source in @($sourceGuard, $sourceGovernor)) {
|
|
83
|
+
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
|
|
84
|
+
throw "Required service file not found: $source"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$serviceAlreadyInstalled = $null -ne (Get-Service -Name $serviceName -ErrorAction SilentlyContinue)
|
|
89
|
+
$budgetAlreadyExists = Test-Path -LiteralPath $budget -PathType Leaf
|
|
90
|
+
if ($budgetAlreadyExists -and -not $serviceAlreadyInstalled) {
|
|
91
|
+
[Console]::Error.WriteLine(
|
|
92
|
+
"Refusing the pre-existing $budget because no installed service proves that it is admin-owned. " +
|
|
93
|
+
'Inspect it, remove it, and run the installer again.'
|
|
94
|
+
)
|
|
95
|
+
exit 3
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Lock-CaddisTree {
|
|
99
|
+
param([Parameter(Mandatory)][string]$Path)
|
|
100
|
+
|
|
101
|
+
$rootItem = Get-Item -LiteralPath $Path -Force
|
|
102
|
+
$reparsePoint = @($rootItem) + @(Get-ChildItem -LiteralPath $Path -Force -Recurse)
|
|
103
|
+
$reparsePoint = $reparsePoint | Where-Object {
|
|
104
|
+
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
|
|
105
|
+
} | Select-Object -First 1
|
|
106
|
+
if ($null -ne $reparsePoint) {
|
|
107
|
+
throw "Refusing reparse point in service tree: $($reparsePoint.FullName)"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Remove inherited and explicit grants from any pre-existing tree before the LocalSystem
|
|
111
|
+
# service can read it. New files then inherit only the three grants below.
|
|
112
|
+
& icacls.exe $Path '/setowner' '*S-1-5-32-544' '/T' '/C'
|
|
113
|
+
if ($LASTEXITCODE -ne 0) {
|
|
114
|
+
throw "icacls owner change failed for $Path with exit code $LASTEXITCODE"
|
|
115
|
+
}
|
|
116
|
+
& icacls.exe $Path '/reset' '/T' '/C'
|
|
117
|
+
if ($LASTEXITCODE -ne 0) {
|
|
118
|
+
throw "icacls reset failed for $Path with exit code $LASTEXITCODE"
|
|
119
|
+
}
|
|
120
|
+
& icacls.exe $Path '/inheritance:r' '/grant:r' '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX'
|
|
121
|
+
if ($LASTEXITCODE -ne 0) {
|
|
122
|
+
throw "icacls lock failed for $Path with exit code $LASTEXITCODE"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Set-CaddisTreeOwner {
|
|
127
|
+
param([Parameter(Mandatory)][string]$Path)
|
|
128
|
+
|
|
129
|
+
& icacls.exe $Path '/setowner' '*S-1-5-32-544' '/T' '/C'
|
|
130
|
+
if ($LASTEXITCODE -ne 0) {
|
|
131
|
+
throw "icacls owner change failed for $Path with exit code $LASTEXITCODE"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Assert-NoPythonPathOverride {
|
|
136
|
+
param([Parameter(Mandatory)][string]$Path)
|
|
137
|
+
|
|
138
|
+
if (Test-Path -LiteralPath $Path -PathType Container) {
|
|
139
|
+
foreach ($pattern in @('python*._pth', 'python*.zip')) {
|
|
140
|
+
$override = Get-ChildItem -LiteralPath $Path -Filter $pattern -File -Force |
|
|
141
|
+
Select-Object -First 1
|
|
142
|
+
if ($null -ne $override) {
|
|
143
|
+
[Console]::Error.WriteLine("Refusing Python path override file: $($override.FullName)")
|
|
144
|
+
exit 3
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function Assert-AdminOwned {
|
|
151
|
+
param(
|
|
152
|
+
[Parameter(Mandatory)][string]$Path,
|
|
153
|
+
[switch]$RequireReadOnlyFile
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
$acl = Get-Acl -LiteralPath $Path
|
|
157
|
+
$ownerSid = ([Security.Principal.NTAccount]$acl.Owner).Translate(
|
|
158
|
+
[Security.Principal.SecurityIdentifier]
|
|
159
|
+
).Value
|
|
160
|
+
$trustedSids = @('S-1-5-18', 'S-1-5-32-544')
|
|
161
|
+
if ($ownerSid -notin $trustedSids) {
|
|
162
|
+
[Console]::Error.WriteLine("Refusing non-admin-owned path: $Path (owner $ownerSid)")
|
|
163
|
+
exit 3
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if ($RequireReadOnlyFile) {
|
|
167
|
+
$writeRights = [Security.AccessControl.FileSystemRights]::Write -bor
|
|
168
|
+
[Security.AccessControl.FileSystemRights]::Delete -bor
|
|
169
|
+
[Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
170
|
+
[Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
171
|
+
[Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
172
|
+
foreach ($rule in $acl.Access) {
|
|
173
|
+
$ruleSid = $rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
|
|
174
|
+
if ($rule.AccessControlType -eq [Security.AccessControl.AccessControlType]::Allow -and
|
|
175
|
+
$ruleSid -notin $trustedSids -and ($rule.FileSystemRights -band $writeRights)) {
|
|
176
|
+
[Console]::Error.WriteLine("Refusing user-writable path: $Path ($ruleSid)")
|
|
177
|
+
exit 3
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Get-TrustedFileHash {
|
|
184
|
+
param([Parameter(Mandatory)][string]$Path)
|
|
185
|
+
|
|
186
|
+
$stream = [IO.File]::Open(
|
|
187
|
+
$Path,
|
|
188
|
+
[IO.FileMode]::Open,
|
|
189
|
+
[IO.FileAccess]::Read,
|
|
190
|
+
[IO.FileShare]::Read
|
|
191
|
+
)
|
|
192
|
+
try {
|
|
193
|
+
# The open handle denies writes and deletes, so this ACL check and hash describe
|
|
194
|
+
# the same file even when the parent directory permits users to add files.
|
|
195
|
+
Assert-AdminOwned -Path $Path -RequireReadOnlyFile
|
|
196
|
+
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
197
|
+
try {
|
|
198
|
+
return [BitConverter]::ToString($sha256.ComputeHash($stream)).Replace('-', '')
|
|
199
|
+
} finally {
|
|
200
|
+
$sha256.Dispose()
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
$stream.Dispose()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function Get-TrustedPythonFiles {
|
|
208
|
+
param([Parameter(Mandatory)][string]$BasePrefix)
|
|
209
|
+
|
|
210
|
+
$trustedFiles = [Collections.Generic.Dictionary[string, string]]::new(
|
|
211
|
+
[StringComparer]::OrdinalIgnoreCase
|
|
212
|
+
)
|
|
213
|
+
$sitePackages = [IO.Path]::GetFullPath((Join-Path $BasePrefix 'Lib\site-packages')).TrimEnd('\') + '\'
|
|
214
|
+
foreach ($item in Get-ChildItem -LiteralPath $BasePrefix -Force -Recurse -ErrorAction Stop) {
|
|
215
|
+
$fullPath = [IO.Path]::GetFullPath($item.FullName)
|
|
216
|
+
if ($fullPath.StartsWith($sitePackages, [StringComparison]::OrdinalIgnoreCase)) {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ($item.PSIsContainer) {
|
|
221
|
+
Assert-AdminOwned -Path $fullPath
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$relativePath = [IO.Path]::GetRelativePath($BasePrefix, $fullPath)
|
|
226
|
+
if ($item.Extension -in @('.dll', '.pyd') -and
|
|
227
|
+
-not $relativePath.StartsWith('tcl\', [StringComparison]::OrdinalIgnoreCase)) {
|
|
228
|
+
$signature = Get-AuthenticodeSignature -LiteralPath $fullPath
|
|
229
|
+
if ($signature.Status -ne 'Valid' -or
|
|
230
|
+
$signature.SignerCertificate.Subject -notmatch
|
|
231
|
+
'O=(?:Python Software Foundation|Microsoft Corporation)(?:,|$)') {
|
|
232
|
+
[Console]::Error.WriteLine("Refusing an unsigned or unexpected Python binary: $fullPath")
|
|
233
|
+
exit 3
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
$trustedFiles[$relativePath] = Get-TrustedFileHash -Path $fullPath
|
|
237
|
+
}
|
|
238
|
+
return ,$trustedFiles
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
$sourcePython = (Get-Command python -CommandType Application -ErrorAction Stop).Source
|
|
242
|
+
$sourcePythonDir = Split-Path -Parent $sourcePython
|
|
243
|
+
Assert-NoPythonPathOverride -Path $sourcePythonDir
|
|
244
|
+
$sourcePythonItem = Get-Item -LiteralPath $sourcePython -Force
|
|
245
|
+
if (($sourcePythonItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
246
|
+
[Console]::Error.WriteLine("Refusing a reparse-point Python executable: $sourcePython")
|
|
247
|
+
exit 3
|
|
248
|
+
}
|
|
249
|
+
Assert-AdminOwned -Path $sourcePython -RequireReadOnlyFile
|
|
250
|
+
$sourceSignature = Get-AuthenticodeSignature -LiteralPath $sourcePython
|
|
251
|
+
if ($sourceSignature.Status -ne 'Valid' -or
|
|
252
|
+
$sourceSignature.SignerCertificate.Subject -notmatch 'O=Python Software Foundation(?:,|$)') {
|
|
253
|
+
[Console]::Error.WriteLine("Refusing an untrusted Python executable: $sourcePython")
|
|
254
|
+
exit 3
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$basePrefix = $sourcePythonDir
|
|
258
|
+
if (-not (Test-Path -LiteralPath (Join-Path $basePrefix 'Lib') -PathType Container) -or
|
|
259
|
+
(Test-Path -LiteralPath (Join-Path (Split-Path -Parent $basePrefix) 'pyvenv.cfg') -PathType Leaf)) {
|
|
260
|
+
[Console]::Error.WriteLine('The Python on PATH must be a base installation, not a virtual environment.')
|
|
261
|
+
exit 3
|
|
262
|
+
}
|
|
263
|
+
$sourceReparsePoint = Get-ChildItem -LiteralPath $basePrefix -Force -Recurse | Where-Object {
|
|
264
|
+
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
|
|
265
|
+
} | Select-Object -First 1
|
|
266
|
+
if ($null -ne $sourceReparsePoint) {
|
|
267
|
+
[Console]::Error.WriteLine("Refusing reparse point in the source Python tree: $($sourceReparsePoint.FullName)")
|
|
268
|
+
exit 3
|
|
269
|
+
}
|
|
270
|
+
Assert-NoPythonPathOverride -Path $basePrefix
|
|
271
|
+
$trustedSourceFiles = Get-TrustedPythonFiles -BasePrefix $basePrefix
|
|
272
|
+
|
|
273
|
+
New-Item -ItemType Directory -Force -Path $programRoot | Out-Null
|
|
274
|
+
New-Item -ItemType Directory -Force -Path $serviceDir | Out-Null
|
|
275
|
+
Lock-CaddisTree -Path $programRoot
|
|
276
|
+
Lock-CaddisTree -Path $serviceDir
|
|
277
|
+
|
|
278
|
+
New-Item -ItemType Directory -Force -Path $nssmDir | Out-Null
|
|
279
|
+
Lock-CaddisTree -Path $nssmDir
|
|
280
|
+
Copy-Item -LiteralPath $sourceNssm -Destination $nssm -Force
|
|
281
|
+
Lock-CaddisTree -Path $nssmDir
|
|
282
|
+
Assert-AdminOwned -Path $nssmDir -RequireReadOnlyFile
|
|
283
|
+
if ((Get-TrustedFileHash -Path $nssm) -ine $NssmSha256) {
|
|
284
|
+
[Console]::Error.WriteLine('The protected NSSM copy does not match -NssmSha256.')
|
|
285
|
+
exit 3
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
& robocopy.exe $basePrefix $pythonDir /MIR /XD 'Lib\site-packages' /XF 'python*._pth' 'python*.zip'
|
|
289
|
+
if ($LASTEXITCODE -ge 8) {
|
|
290
|
+
throw "robocopy failed with exit code $LASTEXITCODE"
|
|
291
|
+
}
|
|
292
|
+
if (-not (Test-Path -LiteralPath $privatePython -PathType Leaf)) {
|
|
293
|
+
throw "The private Python copy does not contain $privatePython"
|
|
294
|
+
}
|
|
295
|
+
Assert-NoPythonPathOverride -Path $pythonDir
|
|
296
|
+
Set-CaddisTreeOwner -Path $programRoot
|
|
297
|
+
$trustedPrivateFiles = Get-TrustedPythonFiles -BasePrefix $pythonDir
|
|
298
|
+
if ($trustedPrivateFiles.Count -ne $trustedSourceFiles.Count) {
|
|
299
|
+
throw 'The private Python copy does not contain the validated source file set.'
|
|
300
|
+
}
|
|
301
|
+
foreach ($entry in $trustedSourceFiles.GetEnumerator()) {
|
|
302
|
+
$privateHash = $null
|
|
303
|
+
if (-not $trustedPrivateFiles.TryGetValue($entry.Key, [ref]$privateHash) -or
|
|
304
|
+
$privateHash -ne $entry.Value) {
|
|
305
|
+
throw "The private Python copy does not match the validated source: $($entry.Key)"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
$privateSignature = Get-AuthenticodeSignature -LiteralPath $privatePython
|
|
309
|
+
if ($privateSignature.Status -ne 'Valid' -or
|
|
310
|
+
$privateSignature.SignerCertificate.Subject -notmatch 'O=Python Software Foundation(?:,|$)') {
|
|
311
|
+
throw "The private Python copy failed signature validation: $privatePython"
|
|
312
|
+
}
|
|
313
|
+
$copiedBasePrefix = (& $privatePython -I -S -c 'import sys; print(sys.base_prefix)').Trim()
|
|
314
|
+
if ($LASTEXITCODE -ne 0 -or $copiedBasePrefix -ine $pythonDir) {
|
|
315
|
+
throw "The private Python resolved sys.base_prefix to $copiedBasePrefix instead of $pythonDir"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
Copy-Item -LiteralPath $sourceGuard -Destination $guardScript -Force
|
|
319
|
+
Copy-Item -LiteralPath $sourceGovernor -Destination $governorScript -Force
|
|
320
|
+
|
|
321
|
+
if (-not (Test-Path -LiteralPath $budget -PathType Leaf)) {
|
|
322
|
+
$budgetText = @"
|
|
323
|
+
[session]
|
|
324
|
+
cpu_percent = 50
|
|
325
|
+
memory_mb = 24576
|
|
326
|
+
max_processes = 256
|
|
327
|
+
|
|
328
|
+
[lane]
|
|
329
|
+
cpu_percent = 25
|
|
330
|
+
memory_mb = 8192
|
|
331
|
+
max_processes = 64
|
|
332
|
+
|
|
333
|
+
[guard]
|
|
334
|
+
mode = "$Mode"
|
|
335
|
+
host_cpu_percent = 85
|
|
336
|
+
sustain_seconds = 60
|
|
337
|
+
sample_seconds = 10
|
|
338
|
+
min_free_memory_mb = 4096
|
|
339
|
+
root_images = ["claude.exe", "codex.exe", "agy.exe"]
|
|
340
|
+
"@
|
|
341
|
+
[System.IO.File]::WriteAllText($budget, $budgetText, [System.Text.UTF8Encoding]::new($false))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
$modeReader = 'import pathlib, sys, tomllib; p = pathlib.Path(sys.argv[1]); data = tomllib.loads(p.read_text(encoding="utf-8")); print(data.get("guard", {}).get("mode", "enforce"))'
|
|
345
|
+
$configuredMode = (& $privatePython -I -S -c $modeReader $budget).Trim()
|
|
346
|
+
if ($LASTEXITCODE -ne 0 -or -not $configuredMode) {
|
|
347
|
+
throw "Could not read the guard mode from $budget"
|
|
348
|
+
}
|
|
349
|
+
if ($configuredMode -ne $Mode) {
|
|
350
|
+
[Console]::Error.WriteLine(
|
|
351
|
+
"$budget is in $configuredMode mode, not requested $Mode mode. Change it from an elevated shell and run again."
|
|
352
|
+
)
|
|
353
|
+
exit 3
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Set-CaddisTreeOwner -Path $programRoot
|
|
357
|
+
Set-CaddisTreeOwner -Path $serviceDir
|
|
358
|
+
|
|
359
|
+
if ($serviceAlreadyInstalled) {
|
|
360
|
+
# Updating Application alone leaves the old wrapper in the service ImagePath.
|
|
361
|
+
$service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$serviceName'"
|
|
362
|
+
$change = Invoke-CimMethod -InputObject $service -MethodName Change -Arguments @{
|
|
363
|
+
PathName = '"{0}"' -f $nssm
|
|
364
|
+
}
|
|
365
|
+
if ($change.ReturnValue -ne 0) {
|
|
366
|
+
throw "Service binary update failed with code $($change.ReturnValue)"
|
|
367
|
+
}
|
|
368
|
+
$appParameters = '-I -S "{0}" run --budget "{1}"' -f $guardScript, $budget
|
|
369
|
+
& $nssm set $serviceName Application $privatePython
|
|
370
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm Application failed with exit code $LASTEXITCODE" }
|
|
371
|
+
& $nssm set $serviceName AppParameters $appParameters
|
|
372
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm AppParameters failed with exit code $LASTEXITCODE" }
|
|
373
|
+
} else {
|
|
374
|
+
& $nssm install $serviceName $privatePython '-I' '-S' $guardScript 'run' '--budget' $budget
|
|
375
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm install failed with exit code $LASTEXITCODE" }
|
|
376
|
+
}
|
|
377
|
+
& $nssm set $serviceName AppStdout $stdoutLog
|
|
378
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm AppStdout failed with exit code $LASTEXITCODE" }
|
|
379
|
+
& $nssm set $serviceName AppStderr $stderrLog
|
|
380
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm AppStderr failed with exit code $LASTEXITCODE" }
|
|
381
|
+
& $nssm set $serviceName ObjectName LocalSystem
|
|
382
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm ObjectName failed with exit code $LASTEXITCODE" }
|
|
383
|
+
& $nssm set $serviceName Start SERVICE_AUTO_START
|
|
384
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm Start failed with exit code $LASTEXITCODE" }
|
|
385
|
+
if ($serviceAlreadyInstalled) {
|
|
386
|
+
& $nssm restart $serviceName
|
|
387
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm restart failed with exit code $LASTEXITCODE" }
|
|
388
|
+
} else {
|
|
389
|
+
& $nssm start $serviceName
|
|
390
|
+
if ($LASTEXITCODE -ne 0) { throw "nssm start failed with exit code $LASTEXITCODE" }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
Write-Output "installed in $Mode mode; switch with: set mode in $budget then & `"$nssm`" restart $serviceName"
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
| Skill | Path | When to Use |
|
|
49
49
|
|-------|------|-------------|
|
|
50
50
|
| Code Documentation | `code-documentation/` | Writing effective code documentation - API docs, README files, inline comments, and technical guides. Use for documenting codebases, APIs, or writing developer guides. |
|
|
51
|
+
| Confluence Hla | `confluence-hla/` | Author enterprise High-Level Architecture (HLA) documents in Simplified Technical English and generate Confluence Storage Format XHTML with self-contained PlantUML diagrams, wrapped tables, and zero AI fingerprints. Use when creating high-level architecture designs, architecture documentation for Confluence, or converting markdown architecture to Confluence storage format. |
|
|
51
52
|
| Explainer Doc | `explainer-doc/` | Write a document that hands off work to a team who did NOT do the investigation — an external vendor, another team, a contractor — and who must be able to act on it alone. Use when the user says "write a spec for the X team", "document this for someone outside the team", "explainer doc", "implementation spec for a vendor", "hand this off to Y", or is about to write a design doc whose primary reader did not live through the debugging/design process. NOT for internal specs where the reader already has full context (use technical-writing or writing-plans instead) — this is specifically for the "stranger must follow it alone" case. |
|
|
52
53
|
| Tech Explainer | `tech-explainer/` | >- |
|
|
53
54
|
| Technical Writing | `technical-writing/` | Technical documentation best practices for READMEs, API docs, architecture docs, runbooks, and developer guides. Use when writing or reviewing documentation, creating onboarding guides, or establishing documentation standards. |
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
| Mermaid Db | `mermaid-db/` | Turn a SQL artifact (proc, view, query, .sql file, or table name) into a Mermaid diagram that explains it — git-diffable, saved as .md |
|
|
84
85
|
| Migrate Dir | `migrate-dir/` | Rename this repo's legacy `.claudster/` artifact dir to `.caddis/` — dry-run first, `git mv` to keep history, merges a straggler dir, rewrites live refs. Opt-in, never automatic. |
|
|
85
86
|
| Park | `park/` | File a future-work item in the one backlog — .caddis/parking-lot/ — or list what is open |
|
|
87
|
+
| Pr Merge | `pr-merge/` | End-to-end pull request and merge lane — push feature branch, open or update PR, monitor CI to green, merge to default branch, and clean up the feature branch. |
|
|
86
88
|
| Prd | `prd/` | Capture requirements through structured discovery and write a PRD |
|
|
87
89
|
| Resume | `resume/` | Pop the most recently parked workstream off the stack and resume it at its exact resume point |
|
|
88
90
|
| Review | `review/` | Review someone else's OPEN pull request end to end — what changed, what CI says, what is risky — then approve or request changes. Never approves on its own. |
|